]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] Improve lua_shape error safety
authorVsevolod Stakhov <vsevolod@rspamd.com>
Wed, 19 Nov 2025 09:17:26 +0000 (09:17 +0000)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Wed, 19 Nov 2025 09:47:32 +0000 (09:47 +0000)
- Transform functions wrapped in pcall to catch user errors
- Default value functions wrapped in pcall
- Pattern matching (string.match, lpeg.match) wrapped in pcall
- Unresolved references return validation errors instead of throwing
- Library now never throws Lua errors on invalid input

lualib/lua_shape/core.lua
lualib/lua_shape/registry.lua

index c4ac399647f5318a18b8b1ef75f28879a8c6ca8c..1a07b25944c9d33d3d2585dd8fd1704eaf17e1c0 100644 (file)
@@ -162,7 +162,14 @@ local function check_string(node, value, ctx)
 
   -- Pattern matching
   if opts.pattern then
-    if not string.match(value, opts.pattern) then
+    local ok, match_result = pcall(string.match, value, opts.pattern)
+    if not ok then
+      return false, make_error("pattern_error", ctx.path, {
+        pattern = opts.pattern,
+        error = tostring(match_result)
+      })
+    end
+    if not match_result then
       return false, make_error("constraint_violation", ctx.path, {
         constraint = "pattern",
         pattern = opts.pattern
@@ -173,7 +180,13 @@ local function check_string(node, value, ctx)
   -- lpeg pattern (optional)
   if opts.lpeg then
     local lpeg = require "lpeg"
-    if not lpeg.match(opts.lpeg, value) then
+    local ok, match_result = pcall(lpeg.match, opts.lpeg, value)
+    if not ok then
+      return false, make_error("lpeg_pattern_error", ctx.path, {
+        error = tostring(match_result)
+      })
+    end
+    if not match_result then
       return false, make_error("constraint_violation", ctx.path, {
         constraint = "lpeg_pattern"
       })
@@ -442,9 +455,19 @@ local function check_table(node, value, ctx)
           local default_val = field_spec.default
           -- Support callable defaults: if default is a function, call it
           if type(default_val) == "function" then
-            default_val = default_val()
+            local ok, val = pcall(default_val)
+            if not ok then
+              has_errors = true
+              errors[field_name] = make_error("default_function_error", field_ctx.path, {
+                field = field_name,
+                error = tostring(val)
+              })
+            else
+              result[field_name] = val
+            end
+          else
+            result[field_name] = default_val
           end
-          result[field_name] = default_val
         end
       else
         has_errors = true
@@ -588,17 +611,18 @@ end
 
 local function check_transform(node, value, ctx)
   if ctx.mode == "transform" then
-    -- First validate the original value against the inner schema in check mode
-    local check_ctx = make_context("check", clone_path(ctx.path))
-    local ok, val_or_err = node.inner:_check(value, check_ctx)
-    if not ok then
-      return false, val_or_err
+    -- Apply transformation (protect against errors in user-provided function)
+    local ok_transform, new_value = pcall(node.fn, value)
+    if not ok_transform then
+      return false, make_error("transform_error", ctx.path, {
+        error = tostring(new_value)
+      })
     end
-    -- Then apply transformation and return transformed value
-    local new_value = node.fn(value)
-    return true, new_value
+
+    -- Validate transformed value against inner schema
+    return node.inner:_check(new_value, ctx)
   else
-    -- In check mode, just validate original value
+    -- In check mode, validate original value against inner schema
     return node.inner:_check(value, ctx)
   end
 end
@@ -798,7 +822,10 @@ function T.ref(id, opts)
     ref_id = id,
     opts = opts or {},
     _check = function(node, value, ctx)
-      error("Unresolved reference: " .. id .. ". Use registry to resolve references.")
+      return false, make_error("unresolved_reference", ctx.path, {
+        ref_id = id,
+        message = "Use registry to resolve references before validation"
+      })
     end
   })
 end
index 39ae83d7902926ac1e894dbaf88643b6e4503847..01409988505832ec0f93604739316d15f72b6451 100644 (file)
@@ -102,7 +102,9 @@ function Registry:resolve_schema(schema)
     local ref_id = schema.ref_id
     local target = self.schemas[ref_id]
     if not target then
-      error("Unresolved reference: " .. ref_id)
+      -- Return schema as-is with unresolved reference
+      -- It will error during validation, not during schema registration
+      return schema
     end
     return target.resolved
   end