fix(security): deny Environment/GC and ADO.NET provider namespaces; close reflection-gateway list (GetTypes/EntryPoint/Declared*/DynamicInvoke)

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-09 15:38:12 -04:00
parent dbd3ed2fc7
commit d19ab93d39
4 changed files with 80 additions and 3 deletions
@@ -42,6 +42,23 @@ public static class ScriptTrustPolicy
"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>
@@ -85,6 +102,18 @@ public static class ScriptTrustPolicy
"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>
@@ -115,11 +115,21 @@ public static class ScriptTrustValidator
if (extraReferences != null)
references.AddRange(extraReferences);
// Apply the SAME default namespace imports the site runtime compiles the
// real script with (ScriptTrustPolicy.DefaultImports — "using System;"
// etc.). Without them the semantic pass under-resolves BARE identifiers
// that the running script sees fully bound — e.g. `Environment.Exit(0)` or
// `GC.Collect()`, whose `Environment`/`GC` resolve to the forbidden
// System.Environment/System.GC types only when `using System;` is in
// scope. Matching the runtime's import set closes that blind spot; it can
// only make MORE identifiers resolve to their true (forbidden) namespace,
// never produce a false allow.
var compilation = CSharpCompilation.CreateScriptCompilation(
"TrustValidation",
tree,
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithUsings(ScriptTrustPolicy.DefaultImports));
var model = compilation.GetSemanticModel(tree);