197 lines
7.9 KiB
C#
197 lines
7.9 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>
|
||
/// M3.1: 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 in later tasks (M3.2–M3.5).
|
||
///
|
||
/// <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",
|
||
];
|
||
|
||
/// <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",
|
||
};
|
||
|
||
/// <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.
|
||
/// </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>
|
||
/// 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();
|
||
|
||
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 */ }
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
{
|
||
var loc = asm.Location;
|
||
if (loc.Length == 0 || byPath.ContainsKey(loc) || !File.Exists(loc))
|
||
continue;
|
||
|
||
try { byPath[loc] = MetadataReference.CreateFromFile(loc); }
|
||
catch { /* ignore */ }
|
||
}
|
||
|
||
// Fallback to the minimal set if the TPA list was unavailable (e.g. a
|
||
// single-file/AOT host) so validation still functions.
|
||
return byPath.Count > 0 ? byPath.Values.ToList() : DefaultReferences;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Default namespace imports made available to compiled scripts.
|
||
/// </summary>
|
||
public static readonly string[] DefaultImports =
|
||
[
|
||
"System",
|
||
"System.Collections.Generic",
|
||
"System.Linq",
|
||
"System.Threading.Tasks",
|
||
];
|
||
}
|