Review assemblies and types loaded from runtime-provided names

Rule ND3125 Security

Matches methods that load assemblies from a path or from bytes (Assembly.LoadFrom(), LoadFile(), Load(byte[]), AssemblyLoadContext.LoadFromAssemblyPath(), LoadFromStream()), instantiate a type from assembly and type names (Activator.CreateInstance(assemblyName, typeName)), or, in a method receiving a string, combine Type.GetType(string) with Activator.CreateInstance(Type). Plugin systems legitimately do this, but a path or name that comes from a request, a configuration file, a database or a user-writable directory turns it into remote code execution.

This is CWE-470 Use of Externally-Controlled Input to Select Classes or Code and CWE-829 Inclusion of Functionality from Untrusted Control Sphere (review).

Flagged Code
public object LoadPlugin(string pluginPath, string typeName)
{
   var asm = Assembly.LoadFrom(pluginPath);                  // flagged: path from caller
   return Activator.CreateInstance(asm.GetType(typeName));
}
Fixed Code
static readonly Dictionary<string, Func<IPlugin>> s_Plugins = new()
{
   ["csv"] = () => new CsvPlugin(),
   ["json"] = () => new JsonPlugin()
};
public IPlugin LoadPlugin(string name)
{
   // The name maps to a closed set of known types: nothing is resolved by reflection.
   if (!s_Plugins.TryGetValue(name, out var create)) throw new ArgumentException(name);
   return create();
}

How to fix

Load plugins only from a directory the application owns (not user-writable), verify a signature or hash before loading, and map user-provided names to a whitelist of known types instead of resolving them by reflection. Suppress the issue once the source of the path or name is confirmed trusted.

CQLinq source

This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.

// <Name>Review assemblies and types loaded from runtime-provided names</Name>
// <Id>ND3125:ReviewAssembliesAndTypesLoadedFromRuntimeProvidedNames</Id>
warnif count > 0

// Third-party members are part of the code model only when the application uses them.
let dynamicLoadMembers =
   ThirdParty.Types.WithFullName("System.Reflection.Assembly").ChildMethods()
      .Where(m => m.SimpleNameEqualsAny("LoadFrom", "LoadFile", "UnsafeLoadFrom", "LoadWithPartialName") ||
                 (m.SimpleName == "Load" && m.Name.Contains("Byte[]")))
   .Concat(ThirdParty.Types.WithFullName("System.Runtime.Loader.AssemblyLoadContext").ChildMethods()
      .Where(m => m.SimpleNameEqualsAny("LoadFromAssemblyPath", "LoadFromStream", "LoadFromNativeImagePath")))
   .Concat(ThirdParty.Types.WithFullNameIn("System.Activator", "System.AppDomain").ChildMethods()
      .Where(m => m.SimpleNameEqualsAny("CreateInstance", "CreateInstanceFrom", "CreateInstanceAndUnwrap", "CreateInstanceFromAndUnwrap") &&
                  m.Name.Contains("String,String")))       // (assemblyName, typeName) overloads
   .ToHashSetEx()

// Type.GetType(string) alone is common; it becomes a dynamic instantiation when combined with
// Activator.CreateInstance(Type...) in a method that receives a string (the type name may flow in).
let typeFromName = ThirdParty.Types.WithFullNameIn("System.Type", "System.Reflection.Assembly").ChildMethods()
   .Where(m => m.SimpleName == "GetType" && m.Name.StartsWith("GetType(String")).ToHashSetEx()
let createInstanceFromType = ThirdParty.Types.WithFullName("System.Activator").ChildMethods()
   .Where(m => m.SimpleName == "CreateInstance" && m.Name.StartsWith("CreateInstance(Type")).ToHashSetEx()

where dynamicLoadMembers.Any() || (typeFromName.Any() && createInstanceFromType.Any())

from m in Application.Methods.UsingAny(dynamicLoadMembers.Concat(typeFromName))
let dynamicLoadMembersUsed = m.MethodsCalled.Where(x => dynamicLoadMembers.Contains(x)).ToArray()
let instantiatesTypeByName = m.Name.Contains("String") &&
                             m.MethodsCalled.Any(x => typeFromName.Contains(x)) &&
                             m.MethodsCalled.Any(x => createInstanceFromType.Contains(x))
where dynamicLoadMembersUsed.Length > 0 || instantiatesTypeByName
select new {
  m,
  dynamicLoadMembersUsed,
  instantiatesTypeByName,
  Debt = 20.ToMinutes().ToDebt(),
  Severity = Severity.Medium
}

//<Expl>
// The *{0}* {2?resolves a type by name and instantiates it:loads code dynamically through {1}}.
// If the path, bytes or type name can be influenced by a user or by a
// writable location, arbitrary code runs inside the process.
//</Expl>

//<Desc>
// Matches methods that load assemblies from a path or from bytes
// (*Assembly.LoadFrom()*, *LoadFile()*, *Load(byte[])*,
// *AssemblyLoadContext.LoadFromAssemblyPath()*, *LoadFromStream()*),
// instantiate a type from assembly and type names (*Activator.CreateInstance(assemblyName, typeName)*),
// or, in a method receiving a string, combine *Type.GetType(string)* with
// *Activator.CreateInstance(Type)*. Plugin systems legitimately do this,
// but a path or name that comes from a request, a configuration file, a
// database or a user-writable directory turns it into remote code
// execution.
//
// This is CWE-470 *Use of Externally-Controlled Input to Select Classes
// or Code* and CWE-829 *Inclusion of Functionality from Untrusted Control
// Sphere* (review).
//
// **Flagged Code**
// ```csharp
// public object LoadPlugin(string pluginPath, string typeName)
// {
//    var asm = Assembly.LoadFrom(pluginPath);                  // flagged: path from caller
//    return Activator.CreateInstance(asm.GetType(typeName));
// }
// ```
//
// **Fixed Code**
// ```csharp
// static readonly Dictionary<string, Func<IPlugin>> s_Plugins = new()
// {
//    ["csv"] = () => new CsvPlugin(),
//    ["json"] = () => new JsonPlugin()
// };
// public IPlugin LoadPlugin(string name)
// {
//    // The name maps to a closed set of known types: nothing is resolved by reflection.
//    if (!s_Plugins.TryGetValue(name, out var create)) throw new ArgumentException(name);
//    return create();
// }
// ```
//</Desc>

//<HowToFix>
// Load plugins only from a directory the application owns (not
// user-writable), verify a signature or hash before loading, and map
// user-provided names to a whitelist of known types instead of resolving
// them by reflection. Suppress the issue once the source of the path or
// name is confirmed trusted.
//</HowToFix>

Security: all 39 items