5a878b78d4
Add missing <summary>/<param>/<returns>/<typeparam> tags and switch interface implementations to <inheritdoc/> across 106 files; strip project bookkeeping identifiers (Task NN, #05-TNN, PLAN-04, StoreAndForward-0NN) from shipped code comments while preserving the descriptive rationale. Comment-only: zero code-logic lines changed; solution builds 0/0. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
344 lines
16 KiB
C#
344 lines
16 KiB
C#
using System.IO;
|
|
using System.Reflection;
|
|
using Microsoft.CodeAnalysis;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
|
|
|
/// <summary>
|
|
/// The single authoritative source of truth for the ScadaBridge script
|
|
/// trust model. Previously the forbidden-API deny-list, allowed exceptions,
|
|
/// reflection-gateway member names, default metadata references, and default
|
|
/// imports were duplicated (and disagreed) across four call sites — the
|
|
/// SiteRuntime <c>ScriptCompilationService</c>, the InboundAPI
|
|
/// <c>ForbiddenApiChecker</c>, and the design-time deploy gate. This class
|
|
/// fuses them into one collection set that <see cref="ScriptTrustValidator"/>
|
|
/// and <see cref="RoslynScriptCompiler"/> consume; the four consumers delegate
|
|
/// here.
|
|
///
|
|
/// <para>
|
|
/// The deny-list is intentionally the UNION of the two existing
|
|
/// implementations — it forbids <c>System.Diagnostics.Process</c> (not all of
|
|
/// <c>System.Diagnostics</c>, so <c>Stopwatch</c> stays allowed), all of
|
|
/// <c>System.Net</c>, all of <c>System.Threading</c> except Tasks /
|
|
/// CancellationToken(Source), plus <c>System.Reflection</c>,
|
|
/// <c>System.Runtime.InteropServices</c>, and <c>Microsoft.Win32</c>.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class ScriptTrustPolicy
|
|
{
|
|
/// <summary>
|
|
/// Forbidden API roots. Each entry is matched as a prefix against the
|
|
/// resolved symbol's containing namespace and fully-qualified containing
|
|
/// type — an entry may name a whole namespace ("System.IO") or a single
|
|
/// type ("System.Diagnostics.Process").
|
|
/// </summary>
|
|
public static readonly string[] ForbiddenScopes =
|
|
[
|
|
"System.IO",
|
|
"System.Diagnostics.Process",
|
|
"System.Threading",
|
|
"System.Reflection",
|
|
"System.Net",
|
|
"System.Runtime.InteropServices",
|
|
"Microsoft.Win32",
|
|
// Whole type — no legitimate script use. Exposes process control
|
|
// (Exit/FailFast), the host environment, and secrets via environment
|
|
// variables (SCADABRIDGE_API_KEY). Callers needing a line break use "\n".
|
|
"System.Environment",
|
|
// Whole type — GC control (Collect, KeepAlive, memory pressure knobs) has
|
|
// no legitimate script use.
|
|
"System.GC",
|
|
// Concrete ADO.NET provider namespaces. Scripts reach a DbConnection ONLY
|
|
// via the Database helper, whose abstract System.Data.Common types stay
|
|
// allowed; forbidding the concrete providers closes the arbitrary-host
|
|
// `new SqlConnection("Server=attacker")` channel. Deliberately NOT
|
|
// forbidding System.Data or System.Data.Common broadly — only these
|
|
// concrete provider namespaces.
|
|
"Microsoft.Data",
|
|
"System.Data.SqlClient",
|
|
"System.Data.Odbc",
|
|
"System.Data.OleDb",
|
|
];
|
|
|
|
/// <summary>
|
|
/// Specific namespaces/types allowed even though they sit under a forbidden
|
|
/// root. async/await and cancellation tokens are OK despite
|
|
/// <c>System.Threading</c> being blocked.
|
|
/// </summary>
|
|
public static readonly string[] AllowedExceptions =
|
|
[
|
|
"System.Threading.Tasks",
|
|
"System.Threading.CancellationToken",
|
|
"System.Threading.CancellationTokenSource",
|
|
];
|
|
|
|
/// <summary>
|
|
/// Member names that are reflection gateways. Reaching any of these — even
|
|
/// off a permitted type such as <c>typeof(string)</c> — lets a script
|
|
/// escape the namespace deny-list (obtain an arbitrary <c>Type</c>, load an
|
|
/// assembly, late-bind a method). They are rejected regardless of the
|
|
/// receiver expression.
|
|
/// </summary>
|
|
public static readonly HashSet<string> ReflectionGatewayMembers = new(StringComparer.Ordinal)
|
|
{
|
|
"GetType",
|
|
"GetTypeInfo",
|
|
"Assembly",
|
|
"Module",
|
|
"CreateInstance",
|
|
"InvokeMember",
|
|
"GetMethod",
|
|
"GetMethods",
|
|
"GetConstructor",
|
|
"GetConstructors",
|
|
"GetField",
|
|
"GetFields",
|
|
"GetProperty",
|
|
"GetProperties",
|
|
"GetMember",
|
|
"GetMembers",
|
|
"GetRuntimeMethod",
|
|
"GetRuntimeMethods",
|
|
"MethodHandle",
|
|
"TypeHandle",
|
|
"GetTypes",
|
|
"EntryPoint",
|
|
"DeclaredMethods",
|
|
"DeclaredMembers",
|
|
"DeclaredConstructors",
|
|
"DynamicInvoke",
|
|
// DELIBERATELY EXCLUDED: "Invoke". The syntactic pass rejects a gateway
|
|
// member regardless of receiver, and delegate.Invoke() is a legitimate,
|
|
// common script pattern (Func<>/Action<> invocation). MethodInfo.Invoke
|
|
// (the reflection late-bind) is already caught semantically via the
|
|
// System.Reflection scope, so excluding "Invoke" here avoids a
|
|
// false-positive blast radius without weakening the reflection deny-list.
|
|
};
|
|
|
|
/// <summary>
|
|
/// Bare identifiers that are forbidden outright. <c>dynamic</c> widens
|
|
/// late-bound member access the static walker cannot see through;
|
|
/// <c>Activator</c> has no non-reflection use.
|
|
/// </summary>
|
|
public static readonly HashSet<string> ForbiddenIdentifiers = new(StringComparer.Ordinal)
|
|
{
|
|
"dynamic",
|
|
"Activator",
|
|
};
|
|
|
|
/// <summary>
|
|
/// Assemblies referenced by compiled scripts. Shared between the Roslyn
|
|
/// scripting options and the semantic-analysis compilation built for trust
|
|
/// validation, so the validator resolves symbols against exactly the same
|
|
/// metadata the script is compiled against.
|
|
///
|
|
/// <para>
|
|
/// This is the <b>minimal, runtime-fidelity</b> set. It deliberately does
|
|
/// NOT reference the assemblies that host the forbidden APIs (e.g.
|
|
/// <c>System.Diagnostics.Process.dll</c>, <c>System.Net.Sockets.dll</c>) — a
|
|
/// forbidden type must remain an <i>undefined symbol</i> at compile time so
|
|
/// the <see cref="RoslynScriptCompiler"/> gate independently rejects it. Do
|
|
/// not widen this set with forbidden-API anchor assemblies; that second layer
|
|
/// of defence (a forbidden type fails to bind at execution-time compilation)
|
|
/// depends on it staying minimal. The trust validator's symbol-resolution
|
|
/// needs are met separately by <see cref="AnalysisReferences"/> (which adds
|
|
/// <see cref="ForbiddenAnchorAssemblies"/> on the minimal-fallback path).
|
|
/// </para>
|
|
/// </summary>
|
|
public static readonly IReadOnlyList<Assembly> DefaultAssemblies =
|
|
[
|
|
typeof(object).Assembly,
|
|
typeof(System.Linq.Enumerable).Assembly,
|
|
typeof(System.Math).Assembly,
|
|
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly,
|
|
typeof(DynamicJsonElement).Assembly,
|
|
];
|
|
|
|
/// <summary>
|
|
/// Anchor assemblies that <b>host the forbidden-API types named in
|
|
/// <see cref="ForbiddenScopes"/></b>. Used ONLY to enrich the <i>trust
|
|
/// validator's</i> semantic reference set (<see cref="AnalysisReferences"/>)
|
|
/// when the <c>TRUSTED_PLATFORM_ASSEMBLIES</c> list is unavailable — a
|
|
/// single-file, AOT, or trimmed host. Without these, the minimal fallback set
|
|
/// cannot resolve a <i>bare</i> forbidden type that lives inside an
|
|
/// <i>allowed</i> namespace (the documented case being <c>Process</c> via
|
|
/// <c>using System.Diagnostics;</c>): the symbol resolves to nothing, Pass 1's
|
|
/// syntactic fallback ignores dotless identifiers, and Pass 2 never flags a
|
|
/// bare identifier — so the forbidden reference slips the validator entirely.
|
|
/// Anchoring these assemblies in the fallback keeps the
|
|
/// semantic pass authoritative even in the degraded mode.
|
|
///
|
|
/// <para>
|
|
/// These are <b>never</b> added to <see cref="DefaultReferences"/> — see the
|
|
/// remark on <see cref="DefaultAssemblies"/>. Most forbidden anchor types
|
|
/// (<c>System.IO.File</c>, <c>System.Threading.Thread</c>,
|
|
/// <c>System.Reflection.Assembly</c>,
|
|
/// <c>System.Runtime.InteropServices.Marshal</c>) already live in the same
|
|
/// assembly as <c>typeof(object)</c> (<c>System.Private.CoreLib</c>), so the
|
|
/// minimal set already resolves them; the ones that ship in their own
|
|
/// assemblies — <c>System.Diagnostics.Process</c> and
|
|
/// <c>System.Net.Sockets.Socket</c> — are listed here explicitly.
|
|
/// </para>
|
|
/// </summary>
|
|
public static readonly IReadOnlyList<Assembly> ForbiddenAnchorAssemblies =
|
|
[
|
|
typeof(System.Diagnostics.Process).Assembly,
|
|
typeof(System.IO.File).Assembly,
|
|
typeof(System.Threading.Thread).Assembly,
|
|
typeof(System.Reflection.Assembly).Assembly,
|
|
typeof(System.Net.Sockets.Socket).Assembly,
|
|
typeof(System.Runtime.InteropServices.Marshal).Assembly,
|
|
];
|
|
|
|
/// <summary>
|
|
/// Metadata references for the trust-validation semantic compilation and
|
|
/// the design-time script compilation.
|
|
/// </summary>
|
|
public static readonly IReadOnlyList<MetadataReference> DefaultReferences =
|
|
DefaultAssemblies
|
|
.Select(a => (MetadataReference)MetadataReference.CreateFromFile(a.Location))
|
|
.ToList();
|
|
|
|
/// <summary>
|
|
/// The full trusted-platform reference set used ONLY by
|
|
/// <see cref="ScriptTrustValidator"/>'s semantic analysis — NOT by
|
|
/// <see cref="RoslynScriptCompiler"/>. Unlike <see cref="DefaultReferences"/>
|
|
/// (the minimal, runtime-fidelity set used to decide script <i>validity</i>,
|
|
/// which must mirror exactly what the site runtime compiles against), the
|
|
/// trust validator references the entire framework so that EVERY type a
|
|
/// script names resolves to a real symbol and is judged by its true
|
|
/// namespace. Without this, a forbidden TYPE that sits inside an ALLOWED
|
|
/// namespace and is reached as a bare identifier — the only such case in the
|
|
/// policy being <c>System.Diagnostics.Process</c> via
|
|
/// <c>using System.Diagnostics;</c> — would not resolve against a minimal
|
|
/// reference set and would slip past the semantic pass (still blocked
|
|
/// downstream as an undefined-symbol compile error, but with a misleading
|
|
/// message). Referencing the full framework lets the validator flag it
|
|
/// authoritatively as a forbidden API. Enriching the analysis reference set
|
|
/// can only IMPROVE detection — the verdict is by namespace/type, so more
|
|
/// resolvable symbols means more correct verdicts, never a false allow.
|
|
/// </summary>
|
|
public static readonly IReadOnlyList<MetadataReference> AnalysisReferences = BuildAnalysisReferences();
|
|
|
|
/// <summary>
|
|
/// True when <see cref="AnalysisReferences"/> was built WITHOUT the
|
|
/// trusted-platform-assemblies (TPA) list — i.e. the semantic pass is running
|
|
/// against the minimal fallback set (a single-file/AOT/trimmed host) rather
|
|
/// than the full framework. In this degraded mode the validator still
|
|
/// resolves the documented forbidden anchors (because
|
|
/// <see cref="ForbiddenAnchorAssemblies"/> is folded into the fallback), but
|
|
/// types outside that anchor set may resolve as unknown and rely on the
|
|
/// syntactic pass / downstream compile gate. A warning is also emitted via
|
|
/// <see cref="System.Diagnostics.Trace"/> at type initialisation. Consumers
|
|
/// (and tests) can read this flag to detect / surface the weakened mode.
|
|
/// </summary>
|
|
public static bool AnalysisReferencesDegraded { get; private set; }
|
|
|
|
private static IReadOnlyList<MetadataReference> BuildAnalysisReferences()
|
|
{
|
|
var byPath = new Dictionary<string, MetadataReference>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Trusted platform assemblies = the full framework reference set the host
|
|
// started with; lets the semantic pass resolve any BCL type.
|
|
if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa)
|
|
{
|
|
foreach (var path in tpa.Split(Path.PathSeparator))
|
|
{
|
|
if (path.Length == 0 ||
|
|
!path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
|
|
byPath.ContainsKey(path) ||
|
|
!File.Exists(path))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try { byPath[path] = MetadataReference.CreateFromFile(path); }
|
|
catch { /* skip an unreadable assembly rather than fail validation */ }
|
|
}
|
|
}
|
|
|
|
// The TPA list was unavailable (single-file / AOT / trimmed host) — we
|
|
// are about to fall back to the minimal set, which weakens the semantic
|
|
// pass. Make the degradation LOUD (not silent): record the flag and emit
|
|
// a warning so operators/tests can detect the mode.
|
|
var tpaAvailable = byPath.Count > 0;
|
|
if (!tpaAvailable)
|
|
{
|
|
AnalysisReferencesDegraded = true;
|
|
System.Diagnostics.Trace.TraceWarning(
|
|
"ScriptTrustPolicy: TRUSTED_PLATFORM_ASSEMBLIES unavailable; the script-trust " +
|
|
"semantic pass is running against the MINIMAL fallback reference set (plus the " +
|
|
"forbidden-API anchor assemblies). Symbol resolution is reduced — forbidden anchors " +
|
|
"(Process, Socket, File, Thread, Assembly, Marshal) remain caught, but other types " +
|
|
"may resolve as unknown. This typically indicates a single-file/AOT/trimmed host.");
|
|
}
|
|
|
|
// Ensure app assemblies the script API surface needs are present even if
|
|
// not in the TPA list (e.g. Commons / DynamicJsonElement).
|
|
foreach (var asm in DefaultAssemblies)
|
|
TryAddAssembly(byPath, asm);
|
|
|
|
// On the minimal-fallback path, also fold in the assemblies that HOST the
|
|
// forbidden-API types so a bare forbidden type inside an allowed namespace
|
|
// (the documented `Process` via `using System.Diagnostics;` case) still
|
|
// resolves and is flagged authoritatively by the semantic pass. When the
|
|
// TPA list is present these are already covered, so this only matters in
|
|
// the degraded mode. NOTE: these anchors are added
|
|
// ONLY here, never to DefaultReferences — the compile gate must keep
|
|
// rejecting forbidden types as undefined symbols.
|
|
if (!tpaAvailable)
|
|
{
|
|
foreach (var asm in ForbiddenAnchorAssemblies)
|
|
TryAddAssembly(byPath, asm);
|
|
}
|
|
|
|
// Should never be empty (DefaultAssemblies always resolve), but guard
|
|
// against a pathological host and fall back to the minimal references.
|
|
return byPath.Count > 0 ? byPath.Values.ToList() : DefaultReferences;
|
|
}
|
|
|
|
private static void TryAddAssembly(
|
|
Dictionary<string, MetadataReference> byPath, Assembly asm)
|
|
{
|
|
var loc = asm.Location;
|
|
if (loc.Length == 0 || byPath.ContainsKey(loc) || !File.Exists(loc))
|
|
return;
|
|
|
|
try { byPath[loc] = MetadataReference.CreateFromFile(loc); }
|
|
catch { /* skip an unreadable assembly rather than fail validation */ }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds exactly the reference set the trust validator would use on the
|
|
/// minimal TPA-fallback path: <see cref="DefaultAssemblies"/> plus the
|
|
/// <see cref="ForbiddenAnchorAssemblies"/> that host the forbidden-API types.
|
|
/// This is what <see cref="BuildAnalysisReferences"/> produces when
|
|
/// <c>TRUSTED_PLATFORM_ASSEMBLIES</c> is unavailable. Exposed so the degraded
|
|
/// mode can be exercised directly (e.g. by the adversarial tests proving the
|
|
/// fallback hole is closed) without depending on the host actually
|
|
/// lacking a TPA list.
|
|
/// </summary>
|
|
/// <returns>The minimal fallback reference set (default assemblies plus forbidden-API anchor assemblies).</returns>
|
|
public static IReadOnlyList<MetadataReference> BuildMinimalFallbackReferences()
|
|
{
|
|
var byPath = new Dictionary<string, MetadataReference>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var asm in DefaultAssemblies)
|
|
TryAddAssembly(byPath, asm);
|
|
foreach (var asm in ForbiddenAnchorAssemblies)
|
|
TryAddAssembly(byPath, asm);
|
|
return byPath.Values.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Default namespace imports made available to compiled scripts.
|
|
/// </summary>
|
|
public static readonly string[] DefaultImports =
|
|
[
|
|
"System",
|
|
"System.Collections.Generic",
|
|
"System.Linq",
|
|
"System.Threading.Tasks",
|
|
];
|
|
}
|