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.
|
||||
|
||||
Reference in New Issue
Block a user