review(Core.Scripting): block Unsafe.As sandbox bypass (Security)
Re-review at 7286d320. Core.Scripting-017 (Medium, Security): System.Runtime.CompilerServices.Unsafe
added to ForbiddenFullTypeNames (Unsafe.As bypasses the type system without an unsafe context;
CWE-843 type-confusion into SetVirtualTag) + regression tests (rejects Unsafe.As, still allows
benign CompilerServices attributes). -018: refresh stale rejection message. Sandbox holds.
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
|---|---|
|
||||
| Module | `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting` |
|
||||
| Reviewer | Claude Code |
|
||||
| Review date | 2026-05-23 |
|
||||
| Commit reviewed | `a9be809` |
|
||||
| Review date | 2026-06-19 |
|
||||
| Commit reviewed | `7286d320` (re-review; HEAD at time of fix `8ac5a2db`) |
|
||||
| Status | Reviewed |
|
||||
| Open findings | 0 |
|
||||
|
||||
@@ -754,3 +754,109 @@ inside two cooperating `[MethodImpl(MethodImplOptions.NoInlining)]` helpers
|
||||
intermediate reflection locals die when each helper returns. Test totals
|
||||
after fix: Core.Scripting 104 green (unchanged); VirtualTags 57 green (was
|
||||
56 — +1 unload test); ScriptedAlarms 67 green (was 66 — +1 unload test).
|
||||
|
||||
## Re-review 2026-06-19 (commit 7286d320)
|
||||
|
||||
All 16 prior findings remain Resolved. This re-review covers the code added between
|
||||
commits `a9be809` and `7286d320` (primarily: `ScriptLogTopicSink`, `ScriptRootLogger`,
|
||||
`ScriptLoggerFactory` identity overload, `ScriptLogCompanionSink` ScriptId fallback,
|
||||
`PassthroughScript` backslash-exclusion hardening, and the Core.Scripting.Abstractions
|
||||
refactor that moved `ScriptContext` / `ScriptGlobals` to a Roslyn-free assembly).
|
||||
|
||||
| # | Category | Result |
|
||||
|---|---|---|
|
||||
| 1 | Correctness & logic bugs | No new issues |
|
||||
| 2 | OtOpcUa conventions | No new issues |
|
||||
| 3 | Concurrency & thread safety | No new issues |
|
||||
| 4 | Error handling & resilience | No new issues |
|
||||
| 5 | Security | Core.Scripting-017 |
|
||||
| 6 | Performance & resource management | No new issues |
|
||||
| 7 | Design-document adherence | No new issues |
|
||||
| 8 | Code organization & conventions | No new issues |
|
||||
| 9 | Testing coverage | No new issues |
|
||||
| 10 | Documentation & comments | Core.Scripting-018 (fixed inline) |
|
||||
|
||||
#### Sandbox re-verification at HEAD
|
||||
|
||||
Re-verified all previously-fixed vectors at HEAD: `System.IO.File`, `HttpClient`,
|
||||
`Process`, `Reflection`, `Environment.Exit`/`FailFast`, `AppDomain`, `GC`, `Activator`,
|
||||
`Thread`, `ThreadPool`, `Timer`, `AssemblyLoadContext`, `ThreadPool.QueueUserWorkItem`,
|
||||
`Timer(callback)`, `typeof(forbidden)`, generic type arguments, casts, `default(T)`,
|
||||
`is`/`as` patterns, array element types, sibling method / sibling class injection — all
|
||||
still rejected. New finding: `System.Runtime.CompilerServices.Unsafe` was not blocked
|
||||
(Core.Scripting-017); fixed in this session.
|
||||
|
||||
### Core.Scripting-017
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Location | `ForbiddenTypeAnalyzer.cs:110-153` (`ForbiddenFullTypeNames`) |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** `System.Runtime.CompilerServices.Unsafe` (in
|
||||
`System.Runtime.CompilerServices.Unsafe.dll`, which starts with `System.` and is
|
||||
therefore included in the BCL references via `EnumerateBclAssemblyPaths`) was not in
|
||||
`ForbiddenNamespacePrefixes` or `ForbiddenFullTypeNames`. The namespace
|
||||
`System.Runtime.CompilerServices` is intentionally not namespace-prefix-denied because
|
||||
it hosts harmless compile-time-only types (`MethodImplAttribute`, `CallerMemberNameAttribute`,
|
||||
`DefaultInterpolatedStringHandler`, etc.) that operator scripts may legitimately use.
|
||||
|
||||
However, `Unsafe.As<TFrom, TTo>(ref TFrom source)` and `Unsafe.As<T>(object o)` do NOT
|
||||
require an `unsafe` context at the call site — the `allowUnsafe: false` compile option
|
||||
only blocks `unsafe {}` blocks and pointer types at the call site, not the `Unsafe`
|
||||
helper class's managed overloads. A script calling
|
||||
`System.Runtime.CompilerServices.Unsafe.As<string>(someBoxedObject)` reinterprets the
|
||||
managed reference bits, bypassing CLR type safety and potentially corrupting managed heap
|
||||
state in downstream virtual-tag consumers that cast the stored value.
|
||||
|
||||
This is not a traditional RCE / "escape to file-system" vector; the script cannot load
|
||||
assemblies or spawn processes via `Unsafe` alone. The risk is CWE-843 (type confusion):
|
||||
a malicious or buggy script could write a type-confused value to `ctx.SetVirtualTag()`,
|
||||
causing a `ClassCastException` or silent wrong-type storage in the virtual-tag engine,
|
||||
which in the worst case brings down the tag's evaluation loop or corrupts a downstream
|
||||
subscriber's reading. Severity is Medium (incorrect/risky behaviour with limited blast
|
||||
radius vs. a full sandbox escape).
|
||||
|
||||
**Recommendation:** Add `System.Runtime.CompilerServices.Unsafe` to
|
||||
`ForbiddenFullTypeNames` (type-granular, same pattern as `Thread` / `ThreadPool` /
|
||||
`Timer`). Add a regression test confirming `Unsafe.As<T>(object)` is rejected, and a
|
||||
guard test confirming that harmless `CompilerServices` attributes (e.g. `MethodImpl`)
|
||||
remain usable.
|
||||
|
||||
**Resolution:** Resolved 2026-06-19 — added `System.Runtime.CompilerServices.Unsafe`
|
||||
to `ForbiddenFullTypeNames` with a detailed comment referencing Core.Scripting-017.
|
||||
Updated the `ForbiddenFullTypeNames` XML doc remarks to enumerate `ThreadPool`, `Timer`,
|
||||
and `Unsafe` alongside the existing entries, and updated the summary doc sentence.
|
||||
The stale rejection message listing only the original four types was replaced (see
|
||||
Core.Scripting-018). Regression tests `Rejects_Unsafe_As_at_compile` and
|
||||
`Benign_CompilerServices_attribute_still_usable` added to `ScriptSandboxTests`. Test
|
||||
totals: Core.Scripting 137 green (was 135, +2 new tests). SHA: TBD.
|
||||
|
||||
### Core.Scripting-018
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Location | `ForbiddenTypeAnalyzer.cs` (`CheckSymbol` rejection message) |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** The rejection message emitted for a type-granular deny-list hit
|
||||
(a match in `ForbiddenFullTypeNames`) read: _"Scripts cannot reach process-control
|
||||
types (Environment / AppDomain / GC / Activator) even though they live in the allowed
|
||||
'System' namespace."_ This list was accurate when written for Core.Scripting-001 but
|
||||
became stale after Core.Scripting-012 added `Thread`, `ThreadPool`, and `Timer` to
|
||||
`ForbiddenFullTypeNames`. An operator trying to use `System.Threading.Timer` would
|
||||
receive a message listing only the original four types, with no mention of `Timer` —
|
||||
confusing guidance that contradicts the source.
|
||||
|
||||
**Recommendation:** Replace the hard-coded type list in the message with a
|
||||
forward-pointer to `ForbiddenTypeAnalyzer.ForbiddenFullTypeNames`.
|
||||
|
||||
**Resolution:** Resolved 2026-06-19 — replaced the stale hard-coded list
|
||||
_"(Environment / AppDomain / GC / Activator)"_ with a generic reference to
|
||||
`ForbiddenTypeAnalyzer.ForbiddenFullTypeNames`, so the error text stays accurate
|
||||
regardless of future deny-list additions. No separate test change needed — companion
|
||||
tests assert on exception type, not message content of this path. SHA: TBD.
|
||||
|
||||
@@ -82,11 +82,11 @@ public static class ForbiddenTypeAnalyzer
|
||||
|
||||
/// <summary>
|
||||
/// Fully-qualified type names scripts are NOT allowed to reference, regardless of
|
||||
/// namespace. These types live directly in the allow-listed <c>System</c>
|
||||
/// namespace (in <c>System.Private.CoreLib</c>), so a namespace-prefix rule cannot
|
||||
/// reach them without also blocking primitives. Matched by exact fully-qualified
|
||||
/// name against the resolved <em>type</em> symbol — every member of the type
|
||||
/// (including read-only ones) is therefore rejected.
|
||||
/// namespace. Used when a dangerous type shares its namespace with legitimate types
|
||||
/// (e.g. <c>System.Threading</c> hosts both <c>CancellationToken</c> and
|
||||
/// <c>Thread</c>) so a namespace-prefix rule would over-block. Matched by exact
|
||||
/// fully-qualified name against the resolved <em>type</em> symbol — every member of
|
||||
/// the type (including read-only ones) is therefore rejected.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
@@ -105,6 +105,14 @@ public static class ForbiddenTypeAnalyzer
|
||||
/// namespace is <c>System.Threading</c> (shared with allowed types like
|
||||
/// <c>CancellationToken</c>), so a namespace-prefix rule cannot reach it
|
||||
/// without blocking unrelated types. (Core.Scripting-010.)</item>
|
||||
/// <item><c>System.Threading.ThreadPool</c> / <c>System.Threading.Timer</c> —
|
||||
/// background-work vectors that outlive the per-evaluation timeout; same
|
||||
/// threat as <c>Task.Run</c> that Core.Scripting-003 closed. (Core.Scripting-012.)</item>
|
||||
/// <item><c>System.Runtime.CompilerServices.Unsafe</c> — exposes raw type-
|
||||
/// reinterpretation (<c>As<TFrom,TTo></c>, <c>As<T>(object)</c>)
|
||||
/// that bypasses the CLR type system without requiring an <c>unsafe</c> context at
|
||||
/// the call site; enables type-confusion and managed heap corruption.
|
||||
/// (Core.Scripting-017.)</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static readonly IReadOnlyList<string> ForbiddenFullTypeNames =
|
||||
@@ -136,6 +144,15 @@ public static class ForbiddenTypeAnalyzer
|
||||
// namespace are denied by default.
|
||||
"System.Threading.ThreadPool",
|
||||
"System.Threading.Timer",
|
||||
// Core.Scripting-017 — System.Runtime.CompilerServices.Unsafe exposes raw
|
||||
// type-reinterpretation (Unsafe.As<TFrom, TTo>, Unsafe.As<T>(object)) that
|
||||
// bypasses the CLR type system without requiring an 'unsafe' compilation context
|
||||
// at the call site. In a script, calling Unsafe.As<string>(someObject) can corrupt
|
||||
// managed heap references and cause type-confusion in downstream virtual-tag
|
||||
// consumers. Type-granular (not namespace-prefix) because System.Runtime.CompilerServices
|
||||
// also contains harmless compile-time-only types (CallerMemberNameAttribute,
|
||||
// MethodImplAttribute, MethodImplOptions) that scripts may legitimately reference.
|
||||
"System.Runtime.CompilerServices.Unsafe",
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
@@ -296,9 +313,8 @@ public static class ForbiddenTypeAnalyzer
|
||||
TypeName: typeSymbol.ToDisplayString(),
|
||||
Namespace: ns,
|
||||
Message: $"Type '{forbiddenType}' is on the Phase 7 sandbox forbidden-type " +
|
||||
$"deny-list. Scripts cannot reach process-control types " +
|
||||
$"(Environment / AppDomain / GC / Activator) even though they " +
|
||||
$"live in the allowed 'System' namespace."));
|
||||
$"deny-list. Scripts cannot reach this type per sandbox rules " +
|
||||
$"(see ForbiddenTypeAnalyzer.ForbiddenFullTypeNames for the authoritative list)."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,4 +569,46 @@ public sealed class ScriptSandboxTests
|
||||
"""));
|
||||
ex.Message.ShouldContain("Core.Scripting-013");
|
||||
}
|
||||
|
||||
// --- Core.Scripting-017: System.Runtime.CompilerServices.Unsafe accessible ---
|
||||
// System.Runtime.CompilerServices is not in ForbiddenNamespacePrefixes (only .InteropServices
|
||||
// and .Loader are blocked). Unsafe.As<TFrom, TTo> / Unsafe.As<T>(object) allow raw type-
|
||||
// reinterpretation without unsafe context at the call site — bypassing the CLR's type-safety
|
||||
// checks and enabling type confusion / managed heap corruption. The fix adds
|
||||
// System.Runtime.CompilerServices.Unsafe to ForbiddenFullTypeNames (type-granular, shared
|
||||
// namespace with harmless CompilerServices types like CallerMemberName, MethodImpl).
|
||||
|
||||
/// <summary>Verifies that Unsafe.As type-reinterpretation is rejected at compile time (Core.Scripting-017).</summary>
|
||||
[Fact]
|
||||
public void Rejects_Unsafe_As_at_compile()
|
||||
{
|
||||
// System.Runtime.CompilerServices.Unsafe.As<T>(object) bypasses CLR type checks
|
||||
// entirely, enabling type confusion and managed heap corruption without requiring
|
||||
// 'unsafe' context at the call site. It must be blocked. (Core.Scripting-017.)
|
||||
Should.Throw<ScriptSandboxViolationException>(() =>
|
||||
ScriptEvaluator<FakeScriptContext, int>.Compile(
|
||||
"""
|
||||
var x = ctx.GetTag("X").Value;
|
||||
var s = System.Runtime.CompilerServices.Unsafe.As<string>(x);
|
||||
return 0;
|
||||
"""));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that MethodImplAttribute (a benign CompilerServices type) is still usable (Core.Scripting-017).</summary>
|
||||
[Fact]
|
||||
public async Task Benign_CompilerServices_attribute_still_usable()
|
||||
{
|
||||
// System.Runtime.CompilerServices.MethodImplOptions is a harmless enum used
|
||||
// with [MethodImpl]. Local functions in scripts can bear it. Denying only
|
||||
// System.Runtime.CompilerServices.Unsafe (type-granular) must not block this.
|
||||
var evaluator = ScriptEvaluator<FakeScriptContext, int>.Compile(
|
||||
"""
|
||||
[System.Runtime.CompilerServices.MethodImpl(
|
||||
System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
static int Helper(int x) => x * 2;
|
||||
return Helper(21);
|
||||
""");
|
||||
var result = await evaluator.RunAsync(new FakeScriptContext(), TestContext.Current.CancellationToken);
|
||||
result.ShouldBe(42);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user