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:
+1
-1
@@ -33,7 +33,7 @@ User scripts are compiled via `Microsoft.CodeAnalysis.CSharp` (regular compiler,
|
||||
|
||||
`ForbiddenTypeAnalyzer.ForbiddenNamespacePrefixes` denies `System.IO`, `System.Net`, `System.Diagnostics`, `System.Reflection`, `System.Threading` (the whole namespace — `ForbiddenTypeAnalyzer.AllowedTypesInForbiddenNamespaces` lets only `CancellationToken`, `Interlocked` and `Volatile` back in), `System.Threading.Tasks`, `System.Runtime.InteropServices`, `System.Runtime.Loader`, and `Microsoft.Win32`. Matching is by prefix against the resolved symbol's containing namespace, so `System.Net` catches `System.Net.Http.HttpClient` and every subnamespace. `System.Threading.Tasks` is denied because scripts are synchronous predicates with no legitimate need to start background tasks — a `Task.Run` fan-out would outlive the per-evaluation timeout entirely (Core.Scripting-003). `System.Runtime.Loader` is denied to block `AssemblyLoadContext` / `AssemblyDependencyResolver` — arbitrary DLL loads into the host process (Core.Scripting-012).
|
||||
|
||||
`ForbiddenTypeAnalyzer.ForbiddenFullTypeNames` denies type-granularly: `System.Environment`, `System.AppDomain`, `System.GC`, `System.Activator`, `System.Runtime.CompilerServices.Unsafe`, and `System.Runtime.CompilerServices.RuntimeHelpers`. These live in namespaces that are otherwise allowed — `System` (primitives, `Math`) and `System.Runtime.CompilerServices` (compile-time attributes such as `CallerMemberNameAttribute`) — so a namespace-prefix rule cannot reach them. `Environment.Exit` / `FailFast` terminate the host process outright (Core.Scripting-001); `Unsafe.As` bypasses the type system; `RuntimeHelpers.GetUninitializedObject` builds an instance of any type without a constructor. `Thread` / `ThreadPool` / `Timer` / `SemaphoreSlim` and the rest of `System.Threading` are now caught by the namespace prefix above instead of being listed one by one — a script is a synchronous expression with no use for background work or blocking primitives (Core.Scripting-010 / -012, review R05:10). `ForbiddenTypeAnalyzer.ForbiddenMembers` additionally denies `System.Type`'s by-name reflection entry points (`GetType(string)`, `GetMethod(s)`, `InvokeMember`, and the `GetProperty`/`GetField`/`GetMember`/`GetConstructor`/`GetEvent` families), which would otherwise reach a forbidden type without naming it syntactically.
|
||||
`ForbiddenTypeAnalyzer.ForbiddenFullTypeNames` denies type-granularly: `System.Environment`, `System.AppDomain`, `System.GC`, `System.Activator`, `System.Runtime.CompilerServices.Unsafe`, `System.Runtime.CompilerServices.RuntimeHelpers`, and (lmxopcua#792, process-wide runtime configuration) `System.AppContext` (`SetSwitch`/`SetData` flip BCL switches for the whole process; `GetData`/`BaseDirectory` leak host paths), `System.Console` (redirects/writes the process streams — log through `ctx.Logger`), `System.Runtime.GCSettings`, `System.Runtime.ProfileOptimization`, `System.Text.EncodingProvider`/`CodePagesEncodingProvider`, `System.ComponentModel.TypeDescriptor` and `System.Security.Cryptography.CryptoConfig`. These live in namespaces that are otherwise allowed — `System` (primitives, `Math`) and `System.Runtime.CompilerServices` (compile-time attributes such as `CallerMemberNameAttribute`) — so a namespace-prefix rule cannot reach them. `Environment.Exit` / `FailFast` terminate the host process outright (Core.Scripting-001); `Unsafe.As` bypasses the type system; `RuntimeHelpers.GetUninitializedObject` builds an instance of any type without a constructor. `Thread` / `ThreadPool` / `Timer` / `SemaphoreSlim` and the rest of `System.Threading` are now caught by the namespace prefix above instead of being listed one by one — a script is a synchronous expression with no use for background work or blocking primitives (Core.Scripting-010 / -012, review R05:10). `ForbiddenTypeAnalyzer.ForbiddenMembers` additionally denies `System.Type`'s by-name reflection entry points (`GetType(string)`, `GetMethod(s)`, `InvokeMember`, and the `GetProperty`/`GetField`/`GetMember`/`GetConstructor`/`GetEvent` families), which would otherwise reach a forbidden type without naming it syntactically, and the ambient-state members `CultureInfo.DefaultThreadCurrentCulture` / `DefaultThreadCurrentUICulture` / `CurrentCulture` / `CurrentUICulture` and `TimeZoneInfo.ClearCachedData` (use `CultureInfo.InvariantCulture` / `GetCultureInfo(...)`). Scripts running in the out-of-process ScriptWorker are rejected by the same analyzer at compile.
|
||||
|
||||
#### Known resource limits (accepted trade-offs)
|
||||
|
||||
|
||||
@@ -1133,8 +1133,9 @@ other work on the box, 10 s phases):**
|
||||
hangs, blocks its caller or throws never reaches the caller and is never a mismatch; MaxInFlight drop accounting;
|
||||
inputs copied before `Mirror` returns.
|
||||
- `ShadowModeTests` (real worker): eleven scripts over short / double / string / arrays / DateTime / bool / status /
|
||||
a throw publish exactly what InProcess publishes and match; a deliberately divergent script (it reads
|
||||
`AppContext.BaseDirectory`, so it answers differently in the worker) and a `decimal` result are counted against
|
||||
a throw publish exactly what InProcess publishes and match; a deliberately divergent script (it does a culture-sensitive
|
||||
`string.Compare("a", "B")`: the worker runs `InvariantGlobalization` so it compares ordinally and answers differently;
|
||||
originally it read `AppContext.BaseDirectory`, which lmxopcua#792 then denied) and a `decimal` result are counted against
|
||||
exactly those two scripts (`value`, `unsupported_type`). A runaway that only runs away in the worker is killed twice
|
||||
and quarantined **there** while every published value stays the in-process one, no published evaluation waits more
|
||||
than 250 ms, and its sibling on the killed shard (a victim, not compared) has no mismatch. Alarms: the emission
|
||||
@@ -1146,8 +1147,10 @@ other work on the box, 10 s phases):**
|
||||
VirtualTags 16, Calculation 84, ScriptWorker 160/4 skip, Host 275, Runtime 1002/3 skip, OpcUaServer.IntegrationTests
|
||||
133. windev (Windows VM, scratch dir under `C:\build`): ScriptWorker suite 160 passed / 4 skipped, 0 warnings. The first
|
||||
windev run caught a test bug (a `DateTime` seconds argument past 59 once the slower VM looped longer), fixed before the commit.
|
||||
- Found while writing them: scripts can reach `System.AppContext` (`SetSwitch` / `SetData` / `GetData`) →
|
||||
lmxopcua#792.
|
||||
- Found while writing them: scripts could reach `System.AppContext` (`SetSwitch` / `SetData` / `GetData`) →
|
||||
lmxopcua#792, fixed: `System.AppContext`, `System.Console`, `GCSettings`, `ProfileOptimization`, `EncodingProvider`,
|
||||
`TypeDescriptor`, `CryptoConfig` and the ambient `CultureInfo` / `TimeZoneInfo.ClearCachedData` members are now denied
|
||||
by `ForbiddenTypeAnalyzer` (in-process and in the worker, one analyzer).
|
||||
|
||||
**docker-dev drill** (image `otopcua-host:w34-shadow` built from this branch; `central-1`/`central-2` only; drill rows
|
||||
`w34-*` inserted by SQL: VTs `w34mul` (HR5×3), `w34raw` (`var v = …HR5.Value; return v;` — compiled, returns the boxed
|
||||
|
||||
Reference in New Issue
Block a user