From f5139b1925a046393ec3ec6c3a828c90139346c5 Mon Sep 17 00:00:00 2001 From: Giulia Stocco <98900+gfs@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:54:07 -0700 Subject: [PATCH 1/2] Guard script execution and resolve assemblies safely Defense in depth additional check to stop scripts from running when Analyzer.Options.RunScripts is false and return a clear Violation in validation or a failed OperationResult at runtime. Replace Assembly.Load usage with resolving assembly file paths and adding MetadataReference.CreateFromFile to avoid triggering module initializers; unresolved references now produce a Violation. Added ResolveAssemblyPath helper (checks loaded assemblies, AppContext.BaseDirectory, and runtime directory) and the System.IO import. --- OAT.Scripting/ScriptOperation.cs | 60 +++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/OAT.Scripting/ScriptOperation.cs b/OAT.Scripting/ScriptOperation.cs index 8d3543e5..53af775e 100644 --- a/OAT.Scripting/ScriptOperation.cs +++ b/OAT.Scripting/ScriptOperation.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; @@ -30,6 +31,12 @@ public ScriptOperation(Analyzer analyzer) : base(Operation.Script, analyzer) internal IEnumerable ScriptOperationValidationDelegate(Rule rule, Clause clause) { + if (Analyzer?.Options.RunScripts != true) + { + yield return new Violation(string.Format(Strings.Get("Err_ScriptingDisabled_{0}{1}"), rule.Name, clause.Label ?? rule.Clauses.IndexOf(clause).ToString(CultureInfo.InvariantCulture)), rule, clause); + yield break; + } + if (clause.Script is ScriptData clauseScript) { var issues = new List(); @@ -40,7 +47,21 @@ internal IEnumerable ScriptOperationValidationDelegate(Rule rule, Cla options = options.AddImports(clauseScript.Imports); options = options.AddReferences(typeof(Analyzer).Assembly); - options = options.AddReferences(clauseScript.References.Select(Assembly.Load)); + // Resolve assembly references without Assembly.Load to avoid triggering + // module initializers during validation (which should be side-effect free). + foreach (var reference in clauseScript.References) + { + var resolvedPath = ResolveAssemblyPath(reference); + if (resolvedPath != null) + { + options = options.AddReferences(MetadataReference.CreateFromFile(resolvedPath)); + } + else + { + issues.Add(new Violation(string.Format(Strings.Get("Err_ClauseInvalidLambda_{0}{1}{2}"), rule.Name, clause.Label ?? rule.Clauses.IndexOf(clause).ToString(CultureInfo.InvariantCulture), $"Could not resolve assembly reference '{reference}'"), rule, clause)); + } + } + var script = CSharpScript.Create(clauseScript.Code, globalsType: typeof(OperationArguments), options: options); foreach (var issue in script.Compile()) @@ -67,10 +88,47 @@ internal IEnumerable ScriptOperationValidationDelegate(Rule rule, Cla } } + /// + /// Resolves an assembly name to a file path without loading it into the runtime. + /// This avoids triggering module initializers that Assembly.Load would execute. + /// + private static string? ResolveAssemblyPath(string assemblyName) + { + // Check already-loaded assemblies (no new loading occurs) + var loaded = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => string.Equals(a.GetName().Name, assemblyName, StringComparison.OrdinalIgnoreCase)); + if (loaded != null && !string.IsNullOrEmpty(loaded.Location)) + { + return loaded.Location; + } + + // Try the application base directory + var basePath = Path.Combine(AppContext.BaseDirectory, assemblyName + ".dll"); + if (File.Exists(basePath)) + { + return basePath; + } + + // Try the runtime directory + var runtimeDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); + var runtimePath = Path.Combine(runtimeDir, assemblyName + ".dll"); + if (File.Exists(runtimePath)) + { + return runtimePath; + } + + return null; + } + private Dictionary?> lambdas { get; } = new Dictionary?>(); internal OperationResult ScriptOperationDelegate(Clause clause, object? state1, object? state2, IEnumerable? captures) { + if (Analyzer?.Options.RunScripts != true) + { + return new OperationResult(false, null); + } + if (clause.Script is ScriptData scriptData) { if (!lambdas.ContainsKey(clause.Script)) From 46dafe0a9cf27d3f3703503f6b292921e6898e70 Mon Sep 17 00:00:00 2001 From: Giulia Stocco <98900+gfs@users.noreply.github.com> Date: Wed, 22 Apr 2026 10:59:27 -0700 Subject: [PATCH 2/2] Add regex timeout handling and 5s limit Wrap regex matching in a try/catch to handle RegexMatchTimeoutException and log a warning instead of crashing. Refactor the matching loops to correctly build and return TypedClauseCapture results for state1 and state2, and add a 5 second match timeout when constructing cached Regex instances to prevent long-running/DoS regex evaluations. --- OAT/Operations/RegexOperation.cs | 49 ++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/OAT/Operations/RegexOperation.cs b/OAT/Operations/RegexOperation.cs index 14ed2dc6..d2b410c3 100644 --- a/OAT/Operations/RegexOperation.cs +++ b/OAT/Operations/RegexOperation.cs @@ -71,40 +71,47 @@ internal OperationResult RegexOperationDelegate(Clause clause, object? state1, o if (regex != null) { - foreach (var state in stateOneList) + try { - var matches = regex.Matches(state); - - if (matches.Count > 0 || (matches.Count == 0 && clause.Invert)) + foreach (var state in stateOneList) { - var outmatches = new List(); - foreach (var match in matches) + var matches = regex.Matches(state); + + if (matches.Count > 0 || (matches.Count == 0 && clause.Invert)) { - if (match is Match m) + var outmatches = new List(); + foreach (var match in matches) { - outmatches.Add(m); + if (match is Match m) + { + outmatches.Add(m); + } } + return new OperationResult(true, !clause.Capture ? null : new TypedClauseCapture>(clause, outmatches, state1)); } - return new OperationResult(true, !clause.Capture ? null : new TypedClauseCapture>(clause, outmatches, state1)); } - } - foreach (var state in stateTwoList) - { - var matches = regex.Matches(state); - - if (matches.Count > 0 || (matches.Count == 0 && clause.Invert)) + foreach (var state in stateTwoList) { - var outmatches = new List(); - foreach (var match in matches) + var matches = regex.Matches(state); + + if (matches.Count > 0 || (matches.Count == 0 && clause.Invert)) { - if (match is Match m) + var outmatches = new List(); + foreach (var match in matches) { - outmatches.Add(m); + if (match is Match m) + { + outmatches.Add(m); + } } + return new OperationResult(true, !clause.Capture ? null : new TypedClauseCapture>(clause, outmatches, state2: state2)); } - return new OperationResult(true, !clause.Capture ? null : new TypedClauseCapture>(clause, outmatches, state2: state2)); } } + catch (RegexMatchTimeoutException) + { + Log.Warning("Regex match timed out for pattern {0}. Treating as non-match.", built); + } } } return new OperationResult(false, null); @@ -122,7 +129,7 @@ internal OperationResult RegexOperationDelegate(Clause clause, object? state1, o { try { - RegexCache.TryAdd((built, regexOptions), new Regex(built, regexOptions)); + RegexCache.TryAdd((built, regexOptions), new Regex(built, regexOptions, TimeSpan.FromSeconds(5))); } catch (ArgumentException) {