fix(scripting): deny System.AppContext and other process-wide mutators in the script sandbox (lmxopcua#792)
v2-ci / unit-tests (push) Waiting to run
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
This commit is contained in:
@@ -145,4 +145,68 @@ public sealed class ScriptSandboxHardeningTests
|
||||
"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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,13 @@ namespace ZB.MOM.WW.OtOpcUa.ScriptWorker.Tests;
|
||||
[Trait("Category", "ScriptWorker")]
|
||||
public sealed class ShadowModeTests
|
||||
{
|
||||
/// <summary>True only inside the ScriptWorker (its base directory is <c>script-worker/</c>): a test-only divergence.</summary>
|
||||
private const string InWorker = "System.AppContext.BaseDirectory.Contains(\"script-worker\")";
|
||||
/// <summary>
|
||||
/// True only inside the ScriptWorker: a test-only divergence that needs no sandbox-forbidden API. The worker runs
|
||||
/// with <c>InvariantGlobalization</c> (ordinal string comparison) while the test host does linguistic (ICU/NLS)
|
||||
/// comparison, so a culture-sensitive <c>string.Compare("a", "B")</c> is negative in the host and positive in the
|
||||
/// worker. <see cref="Culture_sensitive_comparison_really_diverges_between_host_and_worker"/> pins the premise.
|
||||
/// </summary>
|
||||
private const string InWorker = "(string.Compare(\"a\", \"B\") > 0)";
|
||||
|
||||
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
||||
|
||||
@@ -60,6 +65,17 @@ public sealed class ShadowModeTests
|
||||
&& System.Collections.StructuralComparisons.StructuralEqualityComparer.Equals(a.Value, b.Value)
|
||||
&& a.Value?.GetType() == b.Value?.GetType();
|
||||
|
||||
[Fact]
|
||||
public async Task Culture_sensitive_comparison_really_diverges_between_host_and_worker()
|
||||
{
|
||||
// Premise of InWorker: the worker is a different process whose globalization mode differs from the host's.
|
||||
string.Compare("a", "B").ShouldBeLessThan(0, "the test host compares linguistically");
|
||||
await using var pool = Scripts.Runner(Scripts.Options());
|
||||
var r = await pool.RunAsync(Scripts.Vt("probe", $"return {InWorker} ? 2 : 1;"), Ct);
|
||||
r.Status.ShouldBe(ScriptRunStatus.Ok, r.Reason);
|
||||
r.Value.ShouldBe(2, "the worker (InvariantGlobalization) compares ordinally");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ordinary_scripts_match_and_a_divergent_and_an_unsupported_one_are_counted_against_exactly_those_scripts()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user