fix(inbound-api): resolve InboundAPI-014..017 — return-value validation, reflection-gateway hardening, deadline-bound routed calls, RouteHelper test coverage
This commit is contained in:
@@ -15,6 +15,21 @@ namespace ScadaLink.InboundAPI;
|
||||
/// sandbox — so a script can fully-qualify any referenced type. This static check
|
||||
/// walks the script syntax tree and rejects any reference to a forbidden namespace,
|
||||
/// whether reached through a <c>using</c> directive or a fully-qualified name.
|
||||
///
|
||||
/// <para>
|
||||
/// InboundAPI-015: a purely namespace-textual deny-list is bypassable because
|
||||
/// reflection is reachable through members of <em>permitted</em> types that never
|
||||
/// spell a forbidden namespace, e.g.
|
||||
/// <c>typeof(string).Assembly.GetType("System.IO.File")</c>. The walker therefore
|
||||
/// also rejects a curated set of reflection-gateway member names (<c>GetType</c>,
|
||||
/// <c>Assembly</c>, <c>GetMethod</c>, <c>InvokeMember</c>, <c>CreateInstance</c>, …)
|
||||
/// and the <c>dynamic</c> keyword. This is hardening of a best-effort static check,
|
||||
/// <strong>not</strong> a true sandbox — a determined script author may still find
|
||||
/// a vector the syntax walker cannot see (see the security notes in
|
||||
/// <c>code-reviews/InboundAPI/findings.md</c>, InboundAPI-015). The check is
|
||||
/// defence-in-depth; genuine containment needs a runtime boundary (restricted
|
||||
/// <c>AssemblyLoadContext</c> / curated reference set / out-of-process sandbox).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ForbiddenApiChecker
|
||||
{
|
||||
@@ -42,6 +57,40 @@ public static class ForbiddenApiChecker
|
||||
"System.Threading.Tasks",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// InboundAPI-015: member names that are reflection gateways. Reaching any of
|
||||
/// these — even off a permitted type such as <c>typeof(string)</c> or a plain
|
||||
/// <c>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. <c>Invoke</c> is deliberately
|
||||
/// excluded because <c>Action</c>/<c>Func</c> delegate invocation is legitimate;
|
||||
/// the reflection <c>MethodInfo.Invoke</c> path is already cut off by rejecting
|
||||
/// the <c>GetMethod</c>/<c>GetConstructor</c> that produces the <c>MethodInfo</c>.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> ForbiddenMemberNames = new(StringComparer.Ordinal)
|
||||
{
|
||||
"GetType", // object.GetType() / Type.GetType(string) — yields a System.Type
|
||||
"GetTypeInfo", // -> TypeInfo (reflection)
|
||||
"Assembly", // Type.Assembly — yields a System.Reflection.Assembly
|
||||
"Module", // Type.Module / MethodBase.Module
|
||||
"CreateInstance", // Activator.CreateInstance / Assembly.CreateInstance
|
||||
"InvokeMember", // Type.InvokeMember — late-bound dispatch
|
||||
"GetMethod",
|
||||
"GetMethods",
|
||||
"GetConstructor",
|
||||
"GetConstructors",
|
||||
"GetField",
|
||||
"GetFields",
|
||||
"GetProperty",
|
||||
"GetProperties",
|
||||
"GetMember",
|
||||
"GetMembers",
|
||||
"GetRuntimeMethod",
|
||||
"GetRuntimeMethods",
|
||||
"MethodHandle", // RuntimeMethodHandle escape
|
||||
"TypeHandle",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Analyses the script source and returns the list of trust-model violations.
|
||||
/// An empty list means the script is acceptable.
|
||||
@@ -115,7 +164,42 @@ public static class ForbiddenApiChecker
|
||||
return;
|
||||
}
|
||||
|
||||
// InboundAPI-015: reject reflection-gateway members regardless of the
|
||||
// receiver. typeof(string).Assembly.GetType("System.IO.File") never
|
||||
// spells a forbidden namespace, but '.Assembly' and '.GetType' do
|
||||
// appear here as the accessed member name.
|
||||
var memberName = node.Name.Identifier.ValueText;
|
||||
if (ForbiddenMemberNames.Contains(memberName))
|
||||
{
|
||||
_violations.Add($"forbidden reflection member access '.{memberName}'");
|
||||
// Still descend: the receiver may contain a further violation.
|
||||
}
|
||||
|
||||
base.VisitMemberAccessExpression(node);
|
||||
}
|
||||
|
||||
public override void VisitIdentifierName(IdentifierNameSyntax node)
|
||||
{
|
||||
// InboundAPI-015: 'dynamic' widens late-bound member access that the
|
||||
// static walker cannot see through — reject its use outright. The
|
||||
// 'dynamic' contextual keyword surfaces as an identifier name.
|
||||
if (node.Identifier.ValueText == "dynamic")
|
||||
{
|
||||
_violations.Add("forbidden use of the 'dynamic' keyword");
|
||||
return;
|
||||
}
|
||||
|
||||
// InboundAPI-015: a bare reference to the reflection entry-point types
|
||||
// (e.g. 'Activator', 'Type') as an identifier. 'Activator' has no
|
||||
// non-reflection use; flag it. ('Type' as an identifier is too broad
|
||||
// to flag here — the gateway members above already cut off its use.)
|
||||
if (node.Identifier.ValueText == "Activator")
|
||||
{
|
||||
_violations.Add("forbidden reflection type reference 'Activator'");
|
||||
return;
|
||||
}
|
||||
|
||||
base.VisitIdentifierName(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user