47

Here is the reason why this question was being asked: www.devplusplus.com/Tests/CSharp/Hello_World.

While similar questions were asked before, the many answers online have several issues:

  1. This must be done ".Net 4.0" style, not legacy mode.
  2. The assembly is in-memory and will only be in memory, it cannot be written to the file system.
  3. I would like to limit all access to the file-system, network, etc.

Something like this:

    var evidence = new Evidence();
    evidence.AddHostEvidence(new Zone(SecurityZone.Internet));
    var permissionSet = SecurityManager.GetStandardSandbox(evidence);

So far, I cannot find a way to create an AppDomain and load an assembly THAT IS NOT ON THE FILE SYSTEM, but rather in RAM.

Again, the reasons why the other solutions didn't work are identified above: 1. Many were for pre-4.0, and 2. Many relied on the ".Load" method pointing to the file system.

Answer 2: I have an assembly reference due to it being generated by the CSharpCodeProvider class, so if you know a way to turn that into a byte array, that would be perfect!

Sample Code to Show The Security Flaw

var provider = new CSharpCodeProvider(new Dictionary<String, String>
    { { "CompilerVersion", "v4.0" } });

var compilerparams = new CompilerParameters
    { GenerateExecutable = false, GenerateInMemory = true, };

var compilerResults = provider.CompileAssemblyFromSource(compilerparams,
    string_Of_Code_From_A_User);

var instanceOfSomeClass = compilerResults.CompiledAssembly
    .CreateInstance(className);

// The 'DoSomething' method can write to the file system and I don't like that!
instanceOfSomeClass.GetType().GetMethod("DoSomething")
    .Invoke(instanceOfSomeClass, null);

So why can't I just save the assembly to a file first?

For two reasons:

  1. This code is on a shared web server with limited permissions to the file-system itself.
  2. This code may need to be run potentially thousands of times, and I don't want 1,000 dlls, even temporarily.
Peter O.
  • 32,158
  • 14
  • 82
  • 96
Timothy Khouri
  • 31,315
  • 21
  • 88
  • 128
  • Can you marshal a byte array to that other appdomain and load the assembly from that? Also, note that if you say "other solutions don't work", please elaborate why they don't. It is sort of implied that other solutions require the assembly to be written to disk, are you sure *all* other solutions have that requirement? – Lasse V. Karlsen May 13 '11 at 21:33
  • 1
    As in you want to use Load with a byte array? Or do you mean you are creating an in-memory one using the emit API? – tyranid May 13 '11 at 21:34
  • 1
    @tyranid That is a very good question, I guess I sort of assumed that the assembly was being loaded from something that could be translated to a byte array. If it is emitted dynamically, the code that emits the assembly might need to run in that other appdomain. – Lasse V. Karlsen May 13 '11 at 21:35
  • @Lasse and @tyranid - The answers to your very good questions have been added to the original question. – Timothy Khouri May 13 '11 at 22:06
  • I think you need to run the code involving the `CSharpCodeProvider` in that other appdomain. Would that solve your problem? (though it might introduce a whole slew of new ones) – Lasse V. Karlsen May 13 '11 at 22:08
  • Can you post the snippet that is generating the assembly as well? – Can Gencer May 13 '11 at 22:14
  • This sounds like a possible security issue to me (essentially executing data/writeable memory). Isn't there any other way to do it? What's your intention? – Mario May 13 '11 at 22:14
  • 1
    I worked on this problem previously and I could not find a way to get at the COFF byte[] without writing the assembly to disk. If you have access issues, you might be able to use isolated storage. Alternately, there might be some way to set up a virtual file path that writes to memory, though I'm not sure. – Dan Bryant May 13 '11 at 22:16
  • @Dan, The COFF byte[] is exactly what I'm looking for... perhaps I should reflect out the "Save" method and write my own? – Timothy Khouri May 13 '11 at 22:20
  • 1
    Couldn't you save the assembly to disk, and then load it into its own appdomain with reduced permissions? Saving the assembly to disk doesn't introduce a security problem, loading it back in unchecked does. – Lasse V. Karlsen May 13 '11 at 22:28
  • 1
    @Timothy, why don't you create the assembly in a temp location, and execute it in a sandbox? What is the reason you want it specifically to be in RAM? – Can Gencer May 13 '11 at 22:29
  • @Lasse and @Can - answered in original question. – Timothy Khouri May 13 '11 at 22:54
  • I think you'll have to wait for C# 5.0 to be able to compile in-memory. Right now it just executes `csc.exe` which means the source and binary both end up on the hard drive. – Gabe May 14 '11 at 01:49

1 Answers1

42

OK, first things first: there's no actual way to use the CSharpCodeProvider to do dynamic compilation of C# source entirely in memory. There are methods that seem to support that functionality, but since the C# compiler is a native executable that cannot run in-process, the source string is saved to a temporary file, the compiler is invoked on that file, and then the resulting assembly is saved to disk and then loaded for you using Assembly.Load.

Secondly, as you've discovered, you should be able to use the Compile method from within the AppDomain to load the assembly and give it the desired permissions. I ran into this same unusual behavior, and after a lot of digging found that it was a bug in the framework. I filed an issue report for it on MS Connect.

Since the framework is already writing to the filesystem anyway, the workaround is to have the assembly written to a temporary file and then loaded as needed. When you load it however, you'll need to temporarily assert permissions in the AppDomain, since you've disallowed access to the file system. Here's an example snippet of that:

new FileIOPermission(FileIOPermissionAccess.Read | FileIOPermissionAccess.PathDiscovery, assemblyPath).Assert();
var assembly = Assembly.LoadFile(assemblyPath);
CodeAccessPermission.RevertAssert();

From there you can use the assembly and reflection to invoke your method. Note that this method lets you hoist the compilation process outside of the sandboxed AppDomain, which is a plus in my opinion.

For reference, here is my Sandbox class created to facilitate the launching of script assemblies in a nice clean separate AppDomain that has limited permissions and can be easily unloaded when necessary:

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

    public 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 string Execute(string assemblyPath, string scriptType, string method, params object[] parameters)
    {
        new FileIOPermission(FileIOPermissionAccess.Read | FileIOPermissionAccess.PathDiscovery, assemblyPath).Assert();
        var assembly = Assembly.LoadFile(assemblyPath);
        CodeAccessPermission.RevertAssert();

        Type type = assembly.GetType(scriptType);
        if (type == null)
            return null;

        var instance = Activator.CreateInstance(type);
        return string.Format("{0}", type.GetMethod(method).Invoke(instance, parameters));
    }
}

Quick note: if you use this method to supply security evidence for the new AppDomain, you need to sign your assembly to give it a strong name.

Note that this works fine when run in process, but if you really want a bullet-proof script environment, you need to go one step further and isolate the script in a separate process to ensure that scripts that do malicious (or just stupid) things like stack overflows, fork bombs, and out of memory situations don't bring down the whole application process. I can give you more information on doing that if you need it.

Timothy Khouri
  • 31,315
  • 21
  • 88
  • 128
MikeP
  • 7,829
  • 33
  • 34
  • OK, I'm still a little confused... if I have access to assert file system permissions, doesn't the instance class that I'm calling have the same rights? – Timothy Khouri May 14 '11 at 00:09
  • No. You're calling Execute() from within your primary assembly, which obviously has permission to run whatever it wants. The script assembly that you loaded has the reduced permissions, so it won't be able to assert them itself. It's easy to try it for yourself and see. – MikeP May 14 '11 at 00:12
  • @MikeP - The sandbox in my case is a console application. How do I sign it? I am getting the error "A null strongname.." – Nick Nov 13 '11 at 14:47