]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] lua_selectors: don't crash on a missing method call
authorVsevolod Stakhov <vsevolod@rspamd.com>
Sun, 5 Jul 2026 11:43:13 +0000 (12:43 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Sun, 5 Jul 2026 11:43:29 +0000 (12:43 +0100)
Selectors like `time:digest` (method syntax used where a transform
was meant) aborted the whole selector at scan time with an uncaught
Lua error:

  Cannot run callback: .../lua_selectors/init.lua: attempt to call
  a nil value

The `:` separator always compiles to a method call on the extracted
value; when the value has no such method, the lookup yields nil and
the unguarded call crashed the callback. Guard the lookup (via pcall,
since indexing some types raises) and yield no value instead, logging
an error that hints at the `.name` transform syntax when the name
matches a known transform. Valid method calls (e.g. `:gsub`,
`:lower`) and table field access are unaffected.

lualib/lua_selectors/init.lua
test/lua/unit/selectors.lua

index bfb012a692d4bcb3074edda782b370385db2c915..a9632bafab835836c755928a5e6e28108fc6f525 100644 (file)
@@ -47,6 +47,11 @@ local function pure_type(ltype)
   return ltype:match('^(.*)_list$')
 end
 
+-- Indexing values of arbitrary types can raise (e.g. numbers), so it is done via pcall
+local function safe_index(obj, key)
+  return obj[key]
+end
+
 local function implicit_tostring(t, ud_or_table)
   if t == 'table' then
     -- Table (very special)
@@ -382,8 +387,20 @@ exports.parse_selector = function(cfg, str)
               -- Plain table field
               ret = inp[method_name]
             else
+              local ok, meth = pcall(safe_index, inp, method_name)
+
+              if not ok or type(meth) ~= 'function' then
+                if transform_function[method_name] then
+                  logger.errx('cannot call method `%s` on %s value; use `.%s` to apply the transform of the same name',
+                      method_name, t, method_name)
+                else
+                  logger.errx('cannot call method `%s` on %s value', method_name, t)
+                end
+                return nil
+              end
+
               -- We call method unpacking arguments and dropping all but the first result returned
-              ret = (inp[method_name](inp, unpack_function(args or E)))
+              ret = (meth(inp, unpack_function(args or E)))
             end
 
             local ret_type = type(ret)
index 72876339bc029102a1b868b3ba4708e3fb027393..1d4f6dd52313d4218e6d99403221346864d88651 100644 (file)
@@ -420,6 +420,27 @@ context("Selectors test", function()
       assert_rspamd_table_eq_sorted({actual = elts, expect = case.expect})
     end)
   end
+
+  -- Method syntax (`:name`) on a value that has no such method must yield
+  -- nothing instead of raising a runtime error
+  local cases_nil = {
+    ["transform used as method"] = {
+      selector = "time:digest",
+    },
+    ["missing method on string"] = {
+      selector = "header('Subject'):totally_missing_method(1)",
+    },
+  }
+  for case_name, case in lua_util.spairs(cases_nil) do
+    test("nil case " .. case_name, function()
+      local sels = lua_selectors.create_selector_closure_fn(nil, cfg, case.selector, nil,
+          function(_, res, _) return res end)
+      assert_not_nil(sels)
+      local ok, res = pcall(sels, task)
+      assert_true(ok)
+      assert_nil(res)
+    end)
+  end
 end)