4

I am writing an extensible server application. I’d like to expose a library that can be consumed by a third party to write extensions to the server application. When I load the 3rd parties assembly I want to block loading it if the assembly uses Reflection.

The goal is to allow the 3rd party library developer to implement server interfaces and the server will execute those implementations, but will block loading the assembly if reflection is used.

The purpose behind this is to maintain a reasonable level of security when loading a 3rd party assembly into the server. I.E. the 3rd party assembly should be restricted and unable to use reflection to gain access to other parts of the application.

I read about assembly Evidence, but I don’t know if that will allow me to glean the information I need to know.

Loligans
  • 497
  • 9
  • 24
  • 2
    What you’re asking is impossible. And code-access-security died last decade. – Dai Oct 08 '22 at 03:51
  • Doesn't there exist a way to analyze an assembly before loading it into the AppDomain? – Loligans Oct 08 '22 at 04:17
  • 2
    Sounds like you want to use reflection to see if some other assembly uses reflection because you dont approve. – Ňɏssa Pøngjǣrdenlarp Oct 08 '22 at 04:54
  • 3
    You may be interested in [How can I protect my private funcs against reflection executing?](https://stackoverflow.com/questions/8357469/how-can-i-protect-my-private-funcs-against-reflection-executing) and [Why does DisablePrivateReflection attribute not work on .net core 2.1, but works perfectly on .net core 2.0?](https://github.com/dotnet/runtime/issues/11811). – shingo Oct 08 '22 at 06:06

1 Answers1

2

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:

  1. Check what assemblies are referenced.
  2. 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}]");
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
BurnsBA
  • 4,347
  • 27
  • 39