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:
@@ -38,6 +38,16 @@ The following namespace/type prefixes are forbidden in all scripts:
|
|||||||
| `System.Net` | Raw network access — forbidden entirely (scripts must use `ExternalSystem.Call`) |
|
| `System.Net` | Raw network access — forbidden entirely (scripts must use `ExternalSystem.Call`) |
|
||||||
| `System.Runtime.InteropServices` | Native interop — forbidden entirely |
|
| `System.Runtime.InteropServices` | Native interop — forbidden entirely |
|
||||||
| `Microsoft.Win32` | Win32 API access — forbidden entirely |
|
| `Microsoft.Win32` | Win32 API access — forbidden entirely |
|
||||||
|
| `System.Environment` | Whole type — forbidden entirely; exposes process control (`Exit`/`FailFast`), the host environment, and secrets via environment variables (e.g. `SCADABRIDGE_API_KEY`). Scripts that need a line break use `"\n"` instead of `Environment.NewLine`. |
|
||||||
|
| `System.GC` | Whole type — forbidden entirely; GC control (`Collect`, `KeepAlive`, memory-pressure knobs) has no legitimate script use. |
|
||||||
|
| `Microsoft.Data` | Concrete ADO.NET provider namespace (`Microsoft.Data.SqlClient`, …) — forbidden; closes the arbitrary-host `new SqlConnection("Server=attacker")` channel |
|
||||||
|
| `System.Data.SqlClient` | Concrete ADO.NET SQL Server provider — forbidden (see provider-namespace posture below) |
|
||||||
|
| `System.Data.Odbc` | Concrete ADO.NET ODBC provider — forbidden |
|
||||||
|
| `System.Data.OleDb` | Concrete ADO.NET OLE DB provider — forbidden |
|
||||||
|
|
||||||
|
##### `System.Data` provider-namespace posture
|
||||||
|
|
||||||
|
Only the **concrete ADO.NET provider namespaces** are forbidden — `Microsoft.Data` (which covers `Microsoft.Data.SqlClient`), `System.Data.SqlClient`, `System.Data.Odbc`, and `System.Data.OleDb`. The abstract `System.Data.Common` types (`DbConnection`, `DbCommand`, …) and `System.Data` broadly are **deliberately NOT forbidden**: scripts reach a database only through the sanctioned `Database` helper, whose surface exposes those abstract `System.Data.Common` types. Forbidding the concrete providers blocks a script from constructing its own connection to an arbitrary host (the `new SqlConnection(...)` channel) while leaving the sanctioned `Database` path intact.
|
||||||
|
|
||||||
#### Allowed exceptions within forbidden scopes
|
#### Allowed exceptions within forbidden scopes
|
||||||
|
|
||||||
@@ -47,13 +57,15 @@ The following types are explicitly allowed despite falling within a forbidden na
|
|||||||
- `System.Threading.CancellationToken` — cooperative cancellation
|
- `System.Threading.CancellationToken` — cooperative cancellation
|
||||||
- `System.Threading.CancellationTokenSource` — cooperative cancellation
|
- `System.Threading.CancellationTokenSource` — cooperative cancellation
|
||||||
|
|
||||||
The scoping rationale: `System.Diagnostics.Process` is the dangerous type (spawns processes); `Stopwatch`, `Debug`, and `Activity` are harmless diagnostic utilities. Forbidding the whole `System.Diagnostics` namespace, as some earlier call sites did, was overly broad.
|
The scoping rationale: `System.Diagnostics.Process` is the dangerous type (spawns processes); `Stopwatch`, `Debug`, and `Activity` are harmless diagnostic utilities. Forbidding the whole `System.Diagnostics` namespace, as some earlier call sites did, was overly broad. In contrast, `System.Environment` and `System.GC` are forbidden as **whole types** because — unlike `System.Diagnostics` — they have no harmless members a script legitimately needs.
|
||||||
|
|
||||||
#### Reflection-gateway members
|
#### Reflection-gateway members
|
||||||
|
|
||||||
The following member names are blocked regardless of the receiver type, to prevent reflection-based bypasses such as `typeof(x).Assembly.GetType("System.IO.File")`:
|
The following member names are blocked regardless of the receiver type, to prevent reflection-based bypasses such as `typeof(x).Assembly.GetType("System.IO.File")`:
|
||||||
|
|
||||||
`GetType`, `GetTypeInfo`, `Assembly`, `Module`, `CreateInstance`, `InvokeMember`, `GetMethod`, `GetMethods`, `GetConstructor`, `GetConstructors`, `GetField`, `GetFields`, `GetProperty`, `GetProperties`, `GetMember`, `GetMembers`, `GetRuntimeMethod`, `GetRuntimeMethods`, `MethodHandle`, `TypeHandle`.
|
`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`.
|
||||||
|
|
||||||
|
`Invoke` is **deliberately excluded** from this list: the syntactic pass rejects a gateway member regardless of receiver, and `delegate.Invoke()` (`Func<>`/`Action<>` invocation) is a legitimate, common script pattern. The reflection late-bind `MethodInfo.Invoke` 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.
|
||||||
|
|
||||||
#### Forbidden identifiers
|
#### Forbidden identifiers
|
||||||
|
|
||||||
@@ -77,6 +89,7 @@ The identifiers `dynamic` and `Activator` are forbidden at any scope, as they pr
|
|||||||
- For each identifier in the syntax tree, resolves the underlying symbol to its fully qualified containing namespace and type name.
|
- For each identifier in the syntax tree, resolves the underlying symbol to its fully qualified containing namespace and type name.
|
||||||
- Flags any symbol whose containing namespace or type matches a forbidden scope in `ScriptTrustPolicy.ForbiddenScopes`, taking `AllowedExceptions` into account.
|
- Flags any symbol whose containing namespace or type matches a forbidden scope in `ScriptTrustPolicy.ForbiddenScopes`, taking `AllowedExceptions` into account.
|
||||||
- Correctly handles aliases (`using X = System.IO.File`), `using static`, and `global::` prefixes — the resolved symbol is checked, not the spelling.
|
- Correctly handles aliases (`using X = System.IO.File`), `using static`, and `global::` prefixes — the resolved symbol is checked, not the spelling.
|
||||||
|
- The validation compilation applies the **same default namespace imports** (`ScriptTrustPolicy.DefaultImports` — `using System;`, `System.Collections.Generic`, `System.Linq`, `System.Threading.Tasks`) that `RoslynScriptCompiler` compiles the real script with. Without them the semantic pass under-resolves **bare** identifiers 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 import set closes that blind spot; it can only make more identifiers resolve to their true (forbidden) namespace, never produce a false allow.
|
||||||
- Because the full reference set is loaded, this pass also catches a forbidden type accessed inside an otherwise-allowed namespace (e.g., bare `Process` after `using System.Diagnostics;`).
|
- Because the full reference set is loaded, this pass also catches a forbidden type accessed inside an otherwise-allowed namespace (e.g., bare `Process` after `using System.Diagnostics;`).
|
||||||
|
|
||||||
##### `AnalysisReferences` vs `DefaultReferences`
|
##### `AnalysisReferences` vs `DefaultReferences`
|
||||||
|
|||||||
@@ -42,6 +42,23 @@ public static class ScriptTrustPolicy
|
|||||||
"System.Net",
|
"System.Net",
|
||||||
"System.Runtime.InteropServices",
|
"System.Runtime.InteropServices",
|
||||||
"Microsoft.Win32",
|
"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>
|
/// <summary>
|
||||||
@@ -85,6 +102,18 @@ public static class ScriptTrustPolicy
|
|||||||
"GetRuntimeMethods",
|
"GetRuntimeMethods",
|
||||||
"MethodHandle",
|
"MethodHandle",
|
||||||
"TypeHandle",
|
"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>
|
/// <summary>
|
||||||
|
|||||||
@@ -115,11 +115,21 @@ public static class ScriptTrustValidator
|
|||||||
if (extraReferences != null)
|
if (extraReferences != null)
|
||||||
references.AddRange(extraReferences);
|
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(
|
var compilation = CSharpCompilation.CreateScriptCompilation(
|
||||||
"TrustValidation",
|
"TrustValidation",
|
||||||
tree,
|
tree,
|
||||||
references,
|
references,
|
||||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||||
|
.WithUsings(ScriptTrustPolicy.DefaultImports));
|
||||||
|
|
||||||
var model = compilation.GetSemanticModel(tree);
|
var model = compilation.GetSemanticModel(tree);
|
||||||
|
|
||||||
|
|||||||
@@ -321,4 +321,29 @@ public class ScriptTrustValidatorTests
|
|||||||
var code = "var s = \"System.IO.File.ReadAllText\";";
|
var code = "var s = \"System.IO.File.ReadAllText\";";
|
||||||
Assert.Empty(ScriptTrustValidator.FindViolations(code));
|
Assert.Empty(ScriptTrustValidator.FindViolations(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- PLAN-05 Task 21: hardened deny-list additions -----------------------
|
||||||
|
|
||||||
|
// Environment (whole type), GC, and the concrete ADO.NET provider namespaces
|
||||||
|
// (Microsoft.Data / System.Data.SqlClient / .Odbc / .OleDb) are now forbidden.
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Environment.Exit(0);")]
|
||||||
|
[InlineData("Environment.FailFast(\"x\");")]
|
||||||
|
[InlineData("var s = Environment.GetEnvironmentVariable(\"SCADABRIDGE_API_KEY\");")]
|
||||||
|
[InlineData("var c = new Microsoft.Data.SqlClient.SqlConnection(\"Server=attacker\");")]
|
||||||
|
public void FindViolations_Flags(string code) => Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
|
||||||
|
|
||||||
|
// Syntactic gateway closure (matters in TPA-degraded fallback mode): these
|
||||||
|
// reflection-gateway members are rejected regardless of receiver.
|
||||||
|
[Theory]
|
||||||
|
[InlineData("typeof(string).Assembly.GetTypes()")]
|
||||||
|
[InlineData("asm.EntryPoint")]
|
||||||
|
public void SyntacticPass_Flags(string code) => Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
|
||||||
|
|
||||||
|
// Must NOT regress legitimate scripts: Task.Delay stays allowed and
|
||||||
|
// delegate.Invoke() is deliberately NOT on the reflection-gateway list.
|
||||||
|
[Theory]
|
||||||
|
[InlineData("await Task.Delay(1);")]
|
||||||
|
[InlineData("Func<int,int> f = x => x; f.Invoke(3);")]
|
||||||
|
public void FindViolations_Allows(string code) => Assert.Empty(ScriptTrustValidator.FindViolations(code));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user