using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Scripting; namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests; /// /// Sandbox hardening (review R05:10): System.Threading is denied as a namespace with a /// type-granular allow-list (CancellationToken, Interlocked, Volatile) so a /// script can no longer park its pool thread on a blocking primitive or touch a system-global kernel /// object; System.Type's reflection entry points (GetType(string), GetMethod*, /// InvokeMember, …) and System.Runtime.CompilerServices.RuntimeHelpers are denied so a /// forbidden type cannot be reached by name without ever being named syntactically. /// [Trait("Category", "Unit")] public sealed class ScriptSandboxHardeningTests { private static ScriptSandboxViolationException Rejects(string source) => Should.Throw(() => ScriptEvaluator.Compile(source)); // ── System.Threading blocking / synchronisation primitives ─────────────────────────────── [Fact] public void Rejects_SemaphoreSlim_Wait() { var ex = Rejects("new System.Threading.SemaphoreSlim(0).Wait(); return 0;"); ex.Rejections.ShouldContain(r => r.TypeName.Contains("SemaphoreSlim") && r.Namespace == "System.Threading"); } [Fact] public void Rejects_SpinWait_SpinUntil() => Rejects("System.Threading.SpinWait.SpinUntil(() => false); return 0;"); [Fact] public void Rejects_Monitor() => Rejects("var o = new object(); System.Threading.Monitor.Enter(o); return 0;"); [Fact] public void Rejects_named_Mutex_system_global_kernel_object() => Rejects("using var m = new System.Threading.Mutex(false, \"Global\\\\otopcua\"); return 0;"); [Fact] public void Rejects_ManualResetEventSlim() => Rejects("var e = new System.Threading.ManualResetEventSlim(false); e.Wait(); return 0;"); [Fact] public void Rejects_EventWaitHandle() => Rejects("var e = new System.Threading.EventWaitHandle(false, System.Threading.EventResetMode.ManualReset); return 0;"); [Fact] public void Rejects_Thread_ThreadPool_and_Timer_still() { Rejects("var t = new System.Threading.Thread(() => { }); return 0;"); Rejects("System.Threading.ThreadPool.QueueUserWorkItem(_ => { }); return 0;"); Rejects("var t = new System.Threading.Timer(_ => { }); return 0;"); } [Fact] public void Rejects_Thread_Sleep() => Rejects("System.Threading.Thread.Sleep(10); return 0;"); // ── the allow-list inside System.Threading ─────────────────────────────────────────────── [Fact] public async Task Allows_CancellationToken() { using var ev = ScriptEvaluator.Compile( "var t = System.Threading.CancellationToken.None; return t.IsCancellationRequested ? 1 : 0;"); (await ev.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken)).ShouldBe(0); } [Fact] public async Task Allows_Interlocked() { using var ev = ScriptEvaluator.Compile( "var x = 41; System.Threading.Interlocked.Increment(ref x); return x;"); (await ev.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken)).ShouldBe(42); } [Fact] public async Task Allows_Volatile() { using var ev = ScriptEvaluator.Compile( "var x = 42; return System.Threading.Volatile.Read(ref x);"); (await ev.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken)).ShouldBe(42); } // ── System.Type reflection entry points ────────────────────────────────────────────────── [Fact] public void Rejects_Type_GetType_by_name() { var ex = Rejects("var t = System.Type.GetType(\"System.IO.File\"); return t is null ? 0 : 1;"); ex.Rejections.ShouldContain(r => r.Message.Contains("GetType")); } [Fact] public void Rejects_Type_GetMethod() => Rejects("var m = typeof(string).GetMethod(\"Trim\"); return m is null ? 0 : 1;"); [Fact] public void Rejects_Type_GetMethods() => Rejects("return typeof(string).GetMethods().Length;"); [Fact] public void Rejects_Type_InvokeMember() => Rejects("""return (int)typeof(string).InvokeMember("Length", System.Reflection.BindingFlags.GetProperty, null, "abc", null)!;"""); [Fact] public void Rejects_Type_GetProperty_GetField_GetMember_GetConstructor_GetEvent() { Rejects("return typeof(string).GetProperty(\"Length\") is null ? 0 : 1;"); Rejects("return typeof(string).GetField(\"Empty\") is null ? 0 : 1;"); Rejects("return typeof(string).GetMember(\"Length\").Length;"); Rejects("return typeof(string).GetConstructors().Length;"); Rejects("return typeof(string).GetEvent(\"x\") is null ? 0 : 1;"); } [Fact] public async Task Allows_instance_GetType_and_Type_metadata_reads() { // object.GetType() is declared on System.Object, and Type.Name / IsValueType are plain metadata // reads — neither is a reflection entry point, and both are common in authored scripts. using var ev = ScriptEvaluator.Compile( "object o = 42; var t = o.GetType(); return t.IsValueType && typeof(int).Name == \"Int32\" ? 1 : 0;"); (await ev.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken)).ShouldBe(1); } // ── RuntimeHelpers ─────────────────────────────────────────────────────────────────────── [Fact] public void Rejects_RuntimeHelpers() { var ex = Rejects( "var o = System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(string)); return o is null ? 0 : 1;"); ex.Rejections.ShouldContain(r => r.TypeName.Contains("RuntimeHelpers")); } [Fact] public async Task Allows_compile_time_only_CompilerServices_attributes() { // The namespace stays open for the harmless compile-time types (MethodImplOptions etc.). using var ev = ScriptEvaluator.Compile( "var o = System.Runtime.CompilerServices.MethodImplOptions.NoInlining; return (int)o;"); (await ev.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken)).ShouldBe(8); } }