]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] lua_selectors: reject selectors that parse only as a prefix
authorVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 10 Jul 2026 13:16:11 +0000 (14:16 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 10 Jul 2026 13:16:11 +0000 (14:16 +0100)
The selector grammar was not anchored to the end of input, so lpeg
matched the longest valid prefix and silently dropped the rest of the
string. E.g. 'from("smtp"):domain:lower' (a second ':' cannot parse)
was accepted and evaluated as 'from("smtp"):domain', and any trailing
garbage after a selector or a ';' list element was ignored. All
consumers (multimap, rbl, ratelimit, settings, reputation, the
controller selector check used by the WebUI) reported such selectors
as valid while evaluating only the prefix.

Append an lpeg.Cp() capture after the grammar (plus optional trailing
whitespace) and make parse_selector require the whole input to be
consumed, logging the position and the unparsed tail otherwise.

User-visible change: selectors that previously loaded thanks to the
silent truncation now fail configuration load with an error pointing
at the offending token. Such selectors were never evaluating as
written, so failing loudly is the correct behaviour.

lualib/lua_selectors/init.lua

index a9632bafab835836c755928a5e6e28108fc6f525..5d3860967beeea6b8657be40c755539b25abb6ca 100644 (file)
@@ -280,7 +280,7 @@ local function make_grammar()
     NAMED_ARG = (l.Ct("") * l.Cg(argument * eqsign * (argument + l.V("LIST_ARGS")) * comma ^ 0) ^ 0),
     LIST_ARGS = l.Ct(tbl_obrace * l.V("LIST_ARG") * tbl_ebrace),
     LIST_ARG = l.Cg(argument * comma ^ 0) ^ 0,
-  }
+  } * spc * l.Cp()
 end
 
 local parser = make_grammar()
@@ -296,6 +296,16 @@ exports.parse_selector = function(cfg, str)
     return nil
   end
 
+  -- The last capture is the parse end position (lpeg.Cp); lpeg matches the
+  -- longest valid prefix, so anything left after that position is a parse
+  -- error, not a selector to be silently ignored
+  local last_pos = table.remove(parsed)
+  if last_pos <= #str then
+    logger.errx(cfg, "invalid selector '%s': unexpected token at position %d ('%s')",
+        str, last_pos, str:sub(last_pos))
+    return nil
+  end
+
   local function check_args(name, schema, args)
     if schema then
       if getmetatable(schema) then