When I load the 3rd parties assembly I want to block loading it if the assembly uses Reflection
You have a hard problem without a good solution. There's not a good way to verify what some library will be doing. You can implement some heuristics (below), but probably someone motivated enough can find a way around these checks. The following applies to dotnet core, but can be implemented on dotnet framework with some minor changes.
This code performs two analyses:
- Check what assemblies are referenced.
- Look at the IL opcodes for method called into banned assemblies.
The "obvious" approach would be to call assembly.GetReferencedAssemblies()
and check if System.Reflection
is there, but actually this fails because reflection is provided by System.Runtime
and there is not a separate assembly for reflection. But you can at least use that process to check other assemblies.
So it seems a closer inspection of the source code is required. The Mono project debugger library, Mono.Cecil
is used. See this question for a related example.
ILSpy might be useful for determining how/what to check against.
I created two dummy projects for testing. These are .net standard libraries, compiled to dll then dropped in the main runtime folder. These will be checked at runtime, without providing a hard reference to the assembly.
Project 1 -- might do something you don't want it to, but at least it's not using reflection!
using System.Net.Sockets;
namespace LibrarySafe
{
public class ModuleLibrarySafe
{
public bool DoSomething(string args)
{
new TcpClient(args.Split(',')[0], int.Parse(args.Split(',')[1])).GetStream().Write(System.Text.Encoding.ASCII.GetBytes("yeah"), 0, 5);
return true;
}
}
}
Project 2 -- uses reflection:
using System.Reflection;
namespace LibraryUnsafe
{
public class ModuleLibraryUnsafe
{
public bool DoSomething(string args)
{
return Assembly.GetExecutingAssembly().DefinedTypes.Any(x => x.FullName == args);
}
}
}
Following is one mitigation strategy (not a solution) for determining if reflection is used. The Console.WriteLine
sections indicate a match against the banned namespaces. I left some comments in areas that will need to be expanded/adapted to your use case. The following is incomplete and does not include some IL that you will need to consider (I left comments for what I know is missing; perhaps there is more I'm not aware of).
Finally, this could use some exception handling, and probably you want a runtime cache or hashset of some sort to keep track of method calls seen before.
Good luck.
using nuget package System.Reflection.MetadataLoadContext
using Mono.Cecil;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
namespace FindReferencedAssemblies
{
internal class Program
{
// List of namespaces that will be checked against.
private static List<string> _unsafeAssemblyNames = new List<string>()
{
"System.Reflection",
};
static void Main(string[] args)
{
string prefix = Directory.GetCurrentDirectory();
// need to pick up required/running dotnet libraries or PathAssemblyResolver will fail.
var requiredAssemblyFilesnames = new List<string>(Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll"));
// now add assemblies to check. This would come from your runtime module to load but is hard coded here.
var toCheckAssemblyFilesnames = new List<string>()
{
Path.Combine(prefix, "LibrarySafe.dll"),
Path.Combine(prefix, "LibraryUnsafe.dll"),
};
var resolver = new PathAssemblyResolver(requiredAssemblyFilesnames.Concat(toCheckAssemblyFilesnames));
// dotnetcore: assembly metadata is only available withing this "using" context
using (var metadataContext = new MetadataLoadContext(resolver))
{
foreach (var filename in toCheckAssemblyFilesnames)
{
// dotnet framework: use Assembly.ReflectionOnlyLoad
var assembly = metadataContext.LoadFromAssemblyPath(filename);
var assemblyShortName = assembly.GetName().Name;
var assemblyFullName = assembly.FullName;
// Check all referenced assemblies against the list of banned namespaces.
var referencedAssemblies = assembly.GetReferencedAssemblies().ToList();
foreach (var assemblyReference in referencedAssemblies)
{
var unsafeAsm = _unsafeAssemblyNames.FirstOrDefault(x => string.Compare(x, assemblyReference.Name) == 0);
if (!string.IsNullOrEmpty(unsafeAsm))
{
Console.WriteLine($"Found unsafe reference to [{unsafeAsm}] in assembly [{assemblyFullName}]");
}
}
// Now switch over to looking at method calls within the assembly.
var cecilAssemblyDefinition = Mono.Cecil.AssemblyDefinition.ReadAssembly(filename);
// Get a list of all types defined within the assembly.
var assemblyTypes = cecilAssemblyDefinition.MainModule.GetTypes()
// here, `StartsWith` may or may not be sufficient
.Where(x => x.FullName.StartsWith(assemblyShortName));
foreach (var type in assemblyTypes)
{
// Get a list of all methods defined on the type.
foreach (var method in type.Methods)
{
// Find references to methods called, from within the method we are considering.
var calledMethods = method.Body.Instructions
.Where(x =>
x.OpCode == Mono.Cecil.Cil.OpCodes.Call
&& x.Operand is MethodReference)
.Select(x => x.Operand)
.Cast<MethodReference>()
.ToList();
// TODO: perform the same check against `Operand is MethodDefinition`
// TODO: perform the same two checks against `x.OpCode == Mono.Cecil.Cil.OpCodes.Callvirt`
// Iterate the list of methods called, and compare against the list of banned namespaces.
foreach (var methodRef in calledMethods)
{
// here, `StartsWith` may or may not be sufficient
var unsafeAsmMatch = _unsafeAssemblyNames.Where(x => methodRef.FullName.StartsWith(x));
foreach (var match in unsafeAsmMatch)
{
Console.WriteLine($"Found unsafe reference to [{match}] in assembly [{assemblyFullName}], method [{method.FullName}]");
}
}
}
}
}
}
}
}
}