2

My application should be scriptable by the users in C#, but the user's script should run in a restricted AppDomain to prevent scripts accidentally causing damage, but I can't really get it to work, and since my understanding of AppDomains is sadly limited, I can't really tell why.

The solution I am currently trying is based on this answer https://stackoverflow.com/a/5998886/276070.

This is a model of my situation (everything except Script.cs residing in a strongly named assembly). Please excuse the wall of code, I could not condense the problem any further.

class Program
{
    static void Main(string[] args)
    {
        // Compile the script
        CodeDomProvider codeProvider = CodeDomProvider.CreateProvider("CSharp");
        CompilerParameters parameters = new CompilerParameters()
        {
            GenerateExecutable = false,
            OutputAssembly = System.IO.Path.GetTempFileName() + ".dll",                         
        };
        parameters.ReferencedAssemblies.Add(Assembly.GetEntryAssembly().Location);

        CompilerResults results = codeProvider.CompileAssemblyFromFile(parameters, "Script.cs");

        // ... here error checks happen ....//                 

        var sandbox = Sandbox.Create();
        var script = (IExecutable)sandbox.CreateInstance(results.PathToAssembly, "Script");

        if(script != null)
            script.Execute();

    }        
}  

public interface IExecutable
{
    void Execute();
}

The Sandbox class:

public class Sandbox : MarshalByRefObject
{
    const string BaseDirectory = "Untrusted";
    const string DomainName = "Sandbox";        

    public static Sandbox Create()
    {
        var setup = new AppDomainSetup()
        {
            ApplicationBase = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, BaseDirectory),
            ApplicationName = DomainName,
            DisallowBindingRedirects = true,
            DisallowCodeDownload = true,
            DisallowPublisherPolicy = true
        };

        var permissions = new PermissionSet(PermissionState.None);
        permissions.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.RestrictedMemberAccess));
        permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));

        var domain = AppDomain.CreateDomain(DomainName, null, setup, permissions,
            typeof(Sandbox).Assembly.Evidence.GetHostEvidence<StrongName>());

        return (Sandbox)Activator.CreateInstanceFrom(domain, typeof(Sandbox).Assembly.ManifestModule.FullyQualifiedName, typeof(Sandbox).FullName).Unwrap();
    }

    public object CreateInstance(string assemblyPath, string typeName)
    {
        new FileIOPermission(FileIOPermissionAccess.Read | FileIOPermissionAccess.PathDiscovery, assemblyPath).Assert();
        var assembly = Assembly.LoadFile(assemblyPath);
        CodeAccessPermission.RevertAssert();

        Type type = assembly.GetType(typeName); // ****** I get null here
        if (type == null)
            return null;

        return Activator.CreateInstance(type);            
    }
}

The loaded Script:

using System;

public class Script : IExecutable
{
    public void Execute()
    {
        Console.WriteLine("Boo");
    }
}

In CreateInstance of SandBox, I always get null at the marked line. I tried various forms of giving the name, including reading the type name (or fuly qualified name) from results.CompiledAssembly using reflection. What am I doing wrong here?

Community
  • 1
  • 1
Jens
  • 25,229
  • 9
  • 75
  • 117
  • If I am not mistaken when you compile an assembly using CodeDom, it gets loaded in the current AppDomain. You should compile in the child AppDomain and have a MarshalByRefObject derived class that implements IExecutable. This can be stored in another Assembly referenced by both the application and dynamically-compiled assembly. Then use this as the base class for your scripts. This way no type is leaked. By the way the Script class needs to either be MarshalByRefObject derived or serializable to be able to cross AppDomains. – Panos Rontogiannis Aug 21 '13 at 22:52
  • Thanks for the input. I was under the impression that it only got loaded into the current AppDomain if GenerateInMemory is set or the CompilerResults.CompiledAssembly property were used. More research required. =) Compiling inside the new AppDomain throws a SecurityException on CodeDomProvider.CreateProvider unless I use PermissionState.Unrestricted, which in undesirable. I have not found out what permissions are really needed yet. – Jens Aug 22 '13 at 06:52
  • Good luck on your permissions quest. It's a headache. By the way you could compile on a child AppDomain with full permissions, then prepare the sandbox in another. – Panos Rontogiannis Aug 22 '13 at 07:33

1 Answers1

1

The first thing that i'll check is if there are compilation errors (i had several headache caused by this issues)

The second idea is about the resolution of assemblies. I always add as a security check an event handler for AppDomain.CurrentDomain.AssemblyResolve, where i seek on my known path for the missing Assemblies. When the not found assembly is the one i just compiled i add a static reference to it and return it.

What I usually do is this:

  • Create the new Assembly on file system with the compiler
  • Load its content with the File.ReadAllBytes
  • Load the dll with the Assembly.Load in the AppDomain in which i will be using the object
  • Add the AppDomain.CurrentDomain.AssemblyResolve event

Just in case (since i use this a lot) i created a small library to accomply this kind of things

The code and documentation are here: Kendar Expression Builder While the nuget package is here: Nuget Sharp Template

Kendar
  • 692
  • 7
  • 25