fix(scriptanalysis): M3.1 review — Pass 2 self-sufficient descent, pin nested-forbidden + nameof cases, drop dead code

This commit is contained in:
Joseph Doherty
2026-06-16 19:29:59 -04:00
parent 4f2b17ce6d
commit 069c0e0b1a
2 changed files with 80 additions and 10 deletions
@@ -38,6 +38,14 @@ namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
/// is a true sandbox — this is best-effort defence-in-depth; genuine containment
/// needs a runtime boundary.
/// </para>
///
/// <para>
/// A forbidden type reference inside <c>nameof(...)</c> (e.g.
/// <c>nameof(System.IO.File)</c>) is deliberately flagged: this is
/// conservative/fail-safe — <c>nameof</c> is rare in scripts and a script has
/// no business naming a forbidden type even there, so we prefer fail-safe over
/// adding special-case suppression logic to a security trust boundary.
/// </para>
/// </summary>
public static class ScriptTrustValidator
{
@@ -287,11 +295,18 @@ public static class ScriptTrustValidator
var text = StripWhitespace(node.ToString());
// An allowed-exception name (or a strict prefix of one, e.g.
// "System.Threading" under "System.Threading.Tasks") is OK — stop
// descending so the bare forbidden-namespace qualifier of an allowed
// type is not re-examined and falsely flagged.
// "System.Threading" under "System.Threading.Tasks") must NOT be
// reported — but we STILL DESCEND into its children so a forbidden
// type nested inside (e.g. a generic argument under an allowed
// System.Threading.Tasks.TaskCompletionSource<System.Threading.Mutex>)
// is independently visited and flagged by this pass. Suppress the
// outer report, do not stop the walk. (Pass 2 is self-sufficient and
// does not rely on the semantic pass to catch nested forbidden refs.)
if (IsAllowedExceptionPrefix(text))
{
base.VisitQualifiedName(node);
return;
}
// Check the longest qualified name; do not descend so a single
// System.IO.File reference is reported once, not three times.
@@ -323,13 +338,21 @@ public static class ScriptTrustValidator
// Still descend: the receiver may contain a further violation.
}
// An allowed-exception member-access (or a strict prefix of one) is
// OK — stop descending so the bare forbidden-namespace qualifier of
// an allowed type (e.g. the "System.Threading" inside
// "System.Threading.Tasks.Task.Delay") is not re-examined and
// falsely flagged.
// An allowed-exception member-access (or a strict prefix of one)
// must NOT be reported — the bare forbidden-namespace qualifier of an
// allowed type (e.g. the "System.Threading" inside
// "System.Threading.Tasks.Task.Delay") is a false positive. But we
// STILL DESCEND into its children so a forbidden reference nested
// inside (e.g. a System.IO.File access in an allowed
// System.Threading.Tasks.Task.Run(...) lambda body) is independently
// visited and flagged by this pass. Suppress the outer report, do not
// stop the walk. (Pass 2 is self-sufficient — it does not rely on the
// semantic pass to catch nested forbidden refs.)
if (IsAllowedExceptionPrefix(text))
{
base.VisitMemberAccessExpression(node);
return;
}
// Catches fully-qualified expressions such as System.IO.File.Delete(...).
if (IsForbiddenDottedName(text))
@@ -356,8 +379,8 @@ public static class ScriptTrustValidator
}
// A bare reference to a reflection entry-point type. 'Activator' has
// no non-reflection use.
if (ScriptTrustPolicy.ForbiddenIdentifiers.Contains(text) && text != "dynamic")
// no non-reflection use. ('dynamic' already returned above.)
if (ScriptTrustPolicy.ForbiddenIdentifiers.Contains(text))
{
_violations.Add($"forbidden reflection type reference '{text}'");
return;