0e96dd08a2
v2-ci / unit-tests (push) Waiting to run
ForbiddenTypeAnalyzer now denies System.AppContext, System.Console, GCSettings, ProfileOptimization,
EncodingProvider/CodePagesEncodingProvider, TypeDescriptor and CryptoConfig as types, and the ambient
CultureInfo.{DefaultThreadCurrentCulture,DefaultThreadCurrentUICulture,CurrentCulture,CurrentUICulture}
and TimeZoneInfo.ClearCachedData members (member deny-list now covers properties and member-access reads).
Shadow-mode tests diverge via culture-sensitive string.Compare (worker is InvariantGlobalization) instead of
AppContext.BaseDirectory, with a premise test. Mutation-proved: 21 new cases fail with the analyzer reverted.
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
213 lines
11 KiB
C#
213 lines
11 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Tests;
|
|
|
|
/// <summary>
|
|
/// Sandbox hardening (review R05:10): <c>System.Threading</c> is denied as a namespace with a
|
|
/// type-granular allow-list (<c>CancellationToken</c>, <c>Interlocked</c>, <c>Volatile</c>) so a
|
|
/// script can no longer park its pool thread on a blocking primitive or touch a system-global kernel
|
|
/// object; <c>System.Type</c>'s reflection entry points (<c>GetType(string)</c>, <c>GetMethod*</c>,
|
|
/// <c>InvokeMember</c>, …) and <c>System.Runtime.CompilerServices.RuntimeHelpers</c> are denied so a
|
|
/// forbidden type cannot be reached by name without ever being named syntactically.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class ScriptSandboxHardeningTests
|
|
{
|
|
private static ScriptSandboxViolationException Rejects(string source) =>
|
|
Should.Throw<ScriptSandboxViolationException>(() =>
|
|
ScriptEvaluator<FakeScriptContext, int>.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<FakeScriptContext, int>.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<FakeScriptContext, int>.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<FakeScriptContext, int>.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<FakeScriptContext, int>.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<FakeScriptContext, int>.Compile(
|
|
"var o = System.Runtime.CompilerServices.MethodImplOptions.NoInlining; return (int)o;");
|
|
(await ev.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken)).ShouldBe(8);
|
|
}
|
|
|
|
// ── Process-wide mutators (lmxopcua#792) ─────────────────────────────────────────────────
|
|
|
|
[Theory]
|
|
[InlineData("System.AppContext.SetSwitch(\"System.Globalization.Invariant\", true);")]
|
|
[InlineData("System.AppContext.SetData(\"k\", \"v\");")]
|
|
[InlineData("var d = System.AppContext.GetData(\"TRUSTED_PLATFORM_ASSEMBLIES\");")]
|
|
[InlineData("var b = System.AppContext.BaseDirectory;")]
|
|
[InlineData("System.AppContext.TryGetSwitch(\"x\", out var v);")]
|
|
[InlineData("var t = typeof(System.AppContext);")]
|
|
[InlineData("System.Console.SetOut(System.IO.TextWriter.Null);")]
|
|
[InlineData("System.Console.WriteLine(\"x\");")]
|
|
[InlineData("System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.LowLatency;")]
|
|
[InlineData("System.Runtime.ProfileOptimization.SetProfileRoot(\"/tmp\");")]
|
|
[InlineData("System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);")]
|
|
[InlineData("System.ComponentModel.TypeDescriptor.AddAttributes(typeof(string));")]
|
|
[InlineData("System.Security.Cryptography.CryptoConfig.AddAlgorithm(typeof(string), \"x\");")]
|
|
[InlineData("System.Security.Cryptography.CryptoConfig.CreateFromName(\"x\");")]
|
|
[InlineData("System.Globalization.CultureInfo.DefaultThreadCurrentCulture = System.Globalization.CultureInfo.InvariantCulture;")]
|
|
[InlineData("System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = System.Globalization.CultureInfo.InvariantCulture;")]
|
|
[InlineData("System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;")]
|
|
[InlineData("System.Globalization.CultureInfo.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture;")]
|
|
[InlineData("var c = System.Globalization.CultureInfo.CurrentCulture;")]
|
|
[InlineData("System.TimeZoneInfo.ClearCachedData();")]
|
|
[InlineData("System.AppDomain.CurrentDomain.SetData(\"k\", \"v\");")]
|
|
[InlineData("System.Environment.SetEnvironmentVariable(\"k\", \"v\");")]
|
|
[InlineData("System.GC.Collect();")]
|
|
[InlineData("System.Threading.ThreadPool.SetMinThreads(1, 1);")]
|
|
[InlineData("System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;")]
|
|
public void Rejects_process_wide_mutators(string statement)
|
|
{
|
|
var ex = Rejects(statement + " return 0;");
|
|
ex.Rejections.ShouldNotBeEmpty();
|
|
ex.Message.ShouldContain("sandbox");
|
|
}
|
|
|
|
[Fact]
|
|
public void AppContext_rejection_names_the_type()
|
|
{
|
|
var ex = Rejects("System.AppContext.SetSwitch(\"a\", true); return 0;");
|
|
ex.Rejections.ShouldContain(r => r.Message.Contains("System.AppContext") && r.Message.Contains("forbidden-type"));
|
|
}
|
|
|
|
[Fact]
|
|
public void CultureInfo_member_rejection_names_the_member()
|
|
{
|
|
var ex = Rejects("System.Globalization.CultureInfo.DefaultThreadCurrentCulture = null; return 0;");
|
|
ex.Rejections.ShouldContain(r => r.TypeName == "System.Globalization.CultureInfo.DefaultThreadCurrentCulture");
|
|
}
|
|
|
|
[Fact]
|
|
public void Legitimate_culture_and_time_usage_still_compiles()
|
|
{
|
|
// CultureInfo itself, its Invariant / named-lookup entry points and ordinary formatting stay usable.
|
|
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
|
"""
|
|
var inv = System.Globalization.CultureInfo.InvariantCulture;
|
|
var de = System.Globalization.CultureInfo.GetCultureInfo("de-DE");
|
|
var s = (1.5).ToString("F2", inv) + 2.5.ToString(de) + string.Format(inv, "{0}", 3);
|
|
var tz = System.TimeZoneInfo.Utc;
|
|
var n = System.TimeZoneInfo.ConvertTimeFromUtc(System.DateTime.UtcNow, tz);
|
|
return s.Length + 1 + System.Math.Abs(-1);
|
|
""").ShouldNotBeNull();
|
|
}
|
|
}
|