基于Roslyn限制用户C#脚本的可执行操作范围
限制C#用户脚本的方法调用范围解决方案
核心思路
你的需求本质是完全禁止实例方法调用,仅放行基础数学运算和你可控的静态方法。用Roslyn的CSharpSyntaxVisitor配合语义分析(SemanticModel)就能精准实现,无需复杂的标记接口——先从语法层面拦截所有实例调用,再通过语义验证静态方法是否属于你的白名单范围。
具体实现步骤
语法扫描与实例调用拦截
自定义CSharpSyntaxVisitor,重写VisitInvocationExpression方法:- 识别调用节点的结构:如果是
x.DoSomething()这类形式(即InvocationExpressionSyntax的Expression是MemberAccessExpressionSyntax,且其左节点不是类型名),直接判定为非法实例调用。 - 若要更精准,结合
SemanticModel获取方法符号IMethodSymbol,通过methodSymbol.IsStatic直接判断是否为实例方法,这种方式能避免变量名与类型名冲突导致的误判。
- 识别调用节点的结构:如果是
静态方法白名单验证
- 维护一个允许的静态类/方法列表(比如你的自定义
MyString、MyMath)。 - 通过
SemanticModel获取静态方法所属的类型,检查该类型是否在白名单内;如果需要用标记接口,也可以通过methodSymbol.ContainingType.GetInterfaces()判断是否实现了你定义的授权接口(如IAuthorizedStaticClass)。
- 维护一个允许的静态类/方法列表(比如你的自定义
基础数学运算放行
加减乘除等基础运算属于BinaryExpressionSyntax,无需额外拦截,默认允许即可,只需递归检查运算表达式的参数是否合法。
示例代码片段
public class ScriptValidator : CSharpSyntaxVisitor<ValidationResult> { private readonly SemanticModel _semanticModel; // 白名单:你可控的静态类 private readonly HashSet<string> _allowedStaticTypeNames = new() { "MyNamespace.MyString", "MyNamespace.MyMath" }; public ScriptValidator(SemanticModel semanticModel) { _semanticModel = semanticModel; } public override ValidationResult VisitInvocationExpression(InvocationExpressionSyntax node) { var symbolInfo = _semanticModel.GetSymbolInfo(node); if (symbolInfo.Symbol is not IMethodSymbol methodSymbol) { return ValidationResult.Fail("无法识别的方法调用", node.Span); } // 禁止所有实例方法调用 if (!methodSymbol.IsStatic) { return ValidationResult.Fail("不允许调用实例方法", node.Span); } // 验证静态方法所属类是否在白名单 var typeFullName = methodSymbol.ContainingType.ToDisplayString(); if (!_allowedStaticTypeNames.Contains(typeFullName)) { return ValidationResult.Fail($"禁止调用未授权类 {typeFullName} 的方法", node.Span); } // 递归检查参数合法性 foreach (var arg in node.ArgumentList.Arguments) { var argResult = Visit(arg.Expression); if (!argResult.IsValid) return argResult; } return ValidationResult.Success(); } // 放行基础数学运算,递归检查操作数 public override ValidationResult VisitBinaryExpression(BinaryExpressionSyntax node) { var leftResult = Visit(node.Left); if (!leftResult.IsValid) return leftResult; var rightResult = Visit(node.Right); if (!rightResult.IsValid) return rightResult; return ValidationResult.Success(); } // 其他语法节点默认放行(需根据需求调整,比如禁止变量声明等) public override ValidationResult DefaultVisit(SyntaxNode node) { return ValidationResult.Success(); } } // 辅助验证结果类 public class ValidationResult { public bool IsValid { get; } public string ErrorMessage { get; } public TextSpan ErrorSpan { get; } private ValidationResult(bool isValid, string errorMessage, TextSpan errorSpan) { IsValid = isValid; ErrorMessage = errorMessage; ErrorSpan = errorSpan; } public static ValidationResult Success() => new(true, string.Empty, default); public static ValidationResult Fail(string message, TextSpan span) => new(false, message, span); }
补充说明
- 如果不需要语义分析(比如不想提前编译脚本),仅通过语法树判断的话,可以直接检查
MemberAccessExpressionSyntax.Expression的类型:若不是IdentifierNameSyntax(类型名)或QualifiedNameSyntax(带命名空间的类型名),则判定为实例调用,但这种方式可能存在变量名与类型名冲突的误判风险。 - 若要进一步限制静态方法的具体名称,可以在白名单中存储
类型名.方法名的组合,直接匹配methodSymbol.ToDisplayString()。
内容的提问来源于stack exchange,提问作者Will Comeaux
相关产品推荐
相关产品推荐

