如何在编译阶段校验NCalc表达式及引用参数的合法性
NCalc条件表达式参数合法性编译期校验方案
核心逻辑是绕开默认求值的短路逻辑,直接遍历解析完成的语法树提取所有引用的参数,和合法参数白名单做比对,全程不触发表达式求值,从根源避免漏判。
具体实现步骤
- 关闭表达式默认优化
初始化NCalc表达式实例时传入EvaluateOptions.NoOptimize参数,禁用内置的短路求值、常量折叠等优化逻辑,保证语法树完整保留所有用户编写的节点:var expr = new Expression(userInputCondition, EvaluateOptions.NoOptimize); - 递归遍历语法树提取所有参数引用
直接访问表达式解析完成后生成的ParsedExpression语法树根节点,递归遍历所有子节点,把所有标识符(即用户引用的参数)全部收集起来:HashSet<string> GetAllReferencedParameters(LogicalNode rootNode) { var paramSet = new HashSet<string>(StringComparer.Ordinal); void Traverse(LogicalNode node) { switch (node) { case IdentifierNode idNode: paramSet.Add(idNode.Name); break; case BinaryExpressionNode binaryNode: Traverse(binaryNode.LeftExpression); Traverse(binaryNode.RightExpression); break; case UnaryExpressionNode unaryNode: Traverse(unaryNode.Expression); break; case FunctionNode funcNode: foreach (var p in funcNode.Parameters) Traverse(p); break; case TernaryExpressionNode ternaryNode: Traverse(ternaryNode.LeftExpression); Traverse(ternaryNode.MiddleExpression); Traverse(ternaryNode.RightExpression); break; case ValuesListNode listNode: foreach (var p in listNode.Values) Traverse(p); break; // 字面量、常量节点无参数引用,直接跳过 } } Traverse(rootNode); return paramSet; } - 执行参数合法性校验
拿到所有引用的参数集合后,和主程序预先提供的合法参数白名单做差集比对,只要存在差集内容,直接判定为非法表达式,返回对应错误即可,不需要进入后续求值流程:var referencedParams = GetAllReferencedParameters(expr.ParsedExpression); var invalidParams = referencedParams.Except(allowedParamList).ToList(); if (invalidParams.Any()) { throw new ArgumentException($"条件表达式存在非法参数:{string.Join(",", invalidParams)}"); }
注意:所有校验操作必须在调用Evaluate()方法之前完成,整个遍历过程仅做静态语法分析,不会执行任何表达式逻辑,性能开销极低,完全满足编译阶段校验的要求。
原有方案缺陷说明
NCalc自带的
HasErrors()方法仅做语法格式校验,不会检查引用参数的合法性;默认开启的求值优化会对and/or逻辑运算做短路处理,例如执行true or XXXX时,判定左侧为true可直接得到整个表达式结果,就不会解析右侧的XXXX节点,自然无法识别非法参数。
不要尝试通过EvaluateParameter事件做参数校验,短路分支下根本不会触发对应参数的请求事件,依然会存在漏判。
额外扩展
遍历语法树的过程中还可以同步实现其他自定义校验规则,比如限制可用函数列表、禁止特定运算符、校验值类型范围等,完全适配自定义步骤语言的安全管控要求。
内容的提问来源于stack exchange,提问作者Marian
相关产品推荐
相关产品推荐

