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:
Joseph Doherty
2026-06-19 11:06:56 -04:00
parent 65e6af6001
commit 38c48a009c
3 changed files with 174 additions and 10 deletions
+108 -2
View File
@@ -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.