fix(scripting): block ThreadPool/Timer/AssemblyLoadContext in sandbox

Core.Scripting-012 (High, Security) resolution.

The Core.Scripting-008 rewrite broadened the BCL references list from a
narrow allow-list to the full System.* + netstandard +
Microsoft.Win32.Registry set, delegating the security gate entirely to
ForbiddenTypeAnalyzer. Three categories of dangerous BCL types were
reachable from script source without a deny-list entry:

  - System.Threading.ThreadPool — QueueUserWorkItem re-introduces the
    background-fanout threat Core.Scripting-003 closed against
    System.Threading.Tasks.
  - System.Threading.Timer — schedules unbounded callback work that
    outlives the per-evaluation timeout.
  - System.Runtime.Loader.AssemblyLoadContext — loads arbitrary DLLs.
    Defense-in-depth gap; invocation needs reflection (already denied)
    but the load itself was reachable.

Fix:
  - Added 'System.Runtime.Loader' to ForbiddenNamespacePrefixes
    (preferred over type-granular per the recommendation so future BCL
    additions to that namespace are denied by default).
  - Added 'System.Threading.ThreadPool' and 'System.Threading.Timer'
    to ForbiddenFullTypeNames — both live in System.Threading shared
    with allowed primitives so they must be type-granular.

Regression tests added to ScriptSandboxTests:
  Rejects_ThreadPool_QueueUserWorkItem_at_compile
  Rejects_Timer_new_at_compile
  Rejects_AssemblyLoadContext_at_compile

Docs:
  docs/v2/implementation/phase-7-scripting-and-alarming.md decision #6
  and the Sandbox-escape compliance-check row both updated to enumerate
  the new entries per the Core.Scripting-009 doc-sync convention.

Two lower-impact suggestions from the finding's recommendation
(System.Console, CultureInfo.DefaultThreadCurrentCulture) were
intentionally not addressed and are recorded as accepted minor risks
in the resolution.

Verification: Core.Scripting.Tests 107/107 (was 104 + 3 new rejection
tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-23 17:39:20 -04:00
parent fb7c6c7046
commit 3a53d03d23
5 changed files with 97 additions and 7 deletions

View File

@@ -446,4 +446,52 @@ public sealed class ScriptSandboxTests
return 0;
"""));
}
// --- Core.Scripting-012: BCL refs broadened by -008/-016 re-exposed three vectors
// the original deny-list missed. Each is denied type-granularly in
// ForbiddenTypeAnalyzer.ForbiddenFullTypeNames; these tests pin the rejection.
[Fact]
public void Rejects_ThreadPool_QueueUserWorkItem_at_compile()
{
// System.Threading.ThreadPool.QueueUserWorkItem re-introduces the background-
// fanout threat Core.Scripting-003 closed against System.Threading.Tasks. The
// ThreadPool type lives in System.Threading (shared with allowed sync primitives
// like CancellationToken), so it must be denied type-granularly. (Core.Scripting-012.)
Should.Throw<ScriptSandboxViolationException>(() =>
ScriptEvaluator<FakeScriptContext, int>.Compile(
"""
System.Threading.ThreadPool.QueueUserWorkItem(_ => { });
return 0;
"""));
}
[Fact]
public void Rejects_Timer_new_at_compile()
{
// System.Threading.Timer schedules unbounded callback work that outlives the
// per-evaluation timeout. Same namespace as CancellationToken so type-granular.
// (Core.Scripting-012.)
Should.Throw<ScriptSandboxViolationException>(() =>
ScriptEvaluator<FakeScriptContext, int>.Compile(
"""
var t = new System.Threading.Timer(_ => { });
return 0;
"""));
}
[Fact]
public void Rejects_AssemblyLoadContext_at_compile()
{
// System.Runtime.Loader.AssemblyLoadContext loads arbitrary DLLs into the
// process — defense-in-depth gap that the BCL-wide reference set re-opened.
// System.Reflection already denies the typical invocation path, but the
// sandbox boundary stays type-explicit. (Core.Scripting-012.)
Should.Throw<ScriptSandboxViolationException>(() =>
ScriptEvaluator<FakeScriptContext, int>.Compile(
"""
var alc = System.Runtime.Loader.AssemblyLoadContext.Default;
return 0;
"""));
}
}