Search code examples
c#securityexceptionappdomainsandbox

C# AppDomain sandbox security exception when subscribing to domain events


I'm writing a plugin system to run client-provided untrusted code in my server application (C#, .NET 4.0). In order to do this, i'm running each plugin in a new sandboxed AppDomain.

However, I'm stuck on a security exception that I don't really understand the reason for. I have made a streamlined console application sample to illustrate the problem:

namespace SandboxTest
{
    class Program
    {
        static void Main( string[] args )
        {
            Sandbox sandbox = new Sandbox();
            Console.ReadLine();
        }
    }

    class Sandbox
    {
        AppDomain domain;

        public Sandbox()
        {
            PermissionSet ps = new PermissionSet( PermissionState.None );
            ps.AddPermission( new SecurityPermission( SecurityPermissionFlag.Execution ) );

            try
            {
                domain = AppDomain.CreateDomain( "Sandbox", AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.SetupInformation, ps );
                domain.AssemblyLoad += new AssemblyLoadEventHandler( domain_AssemblyLoad );
                domain.AssemblyResolve += new ResolveEventHandler( domain_AssemblyResolve );
            }
            catch( Exception e )
            {
                Trace.WriteLine( e.ToString() );
                throw e;
            }
        }

        static Assembly domain_AssemblyResolve( object sender, ResolveEventArgs args )
        {
            return null;
        }

        static void domain_AssemblyLoad( object sender, AssemblyLoadEventArgs args )
        {

        }
    }
}

Upon running this code, I'm getting the following exception on the domain.AssemblyLoad line:

A first chance exception of type 'System.Security.SecurityException' occurred in SandboxTest.exe
'SandboxTest.vshost.exe' (Managed (v4.0.30319)): Loaded 'C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll', Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
System.Security.SecurityException: Request for the permission of type 'System.Security.Permissions.ReflectionPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
   at System.Security.CodeAccessSecurityEngine.ThrowSecurityException(RuntimeAssembly asm, PermissionSet granted, PermissionSet refused, RuntimeMethodHandleInternal rmh, SecurityAction action, Object demand, IPermission permThatFailed)
   at System.Security.CodeAccessSecurityEngine.ThrowSecurityException(Object assemblyOrString, PermissionSet granted, PermissionSet refused, RuntimeMethodHandleInternal rmh, SecurityAction action, Object demand, IPermission permThatFailed)
   at System.Security.CodeAccessSecurityEngine.CheckHelper(PermissionSet grantedSet, PermissionSet refusedSet, CodeAccessPermission demand, PermissionToken permToken, RuntimeMethodHandleInternal rmh, Object assemblyOrString, SecurityAction action, Boolean throwException)
   at System.Security.CodeAccessSecurityEngine.CheckHelper(CompressedStack cs, PermissionSet grantedSet, PermissionSet refusedSet, CodeAccessPermission demand, PermissionToken permToken, RuntimeMethodHandleInternal rmh, RuntimeAssembly asm, SecurityAction action)
   at System.Security.CodeAccessSecurityEngine.Check(Object demand, StackCrawlMark& stackMark, Boolean isPermSet)
   at System.Security.CodeAccessSecurityEngine.Check(CodeAccessPermission cap, StackCrawlMark& stackMark)
   at System.Security.CodeAccessPermission.Demand()
   at System.DelegateSerializationHolder.GetDelegateSerializationInfo(SerializationInfo info, Type delegateType, Object target, MethodInfo method, Int32 targetIndex)
   at System.MulticastDelegate.GetObjectData(SerializationInfo info, StreamingContext context)
   at System.Runtime.Serialization.ObjectCloneHelper.GetObjectData(Object serObj, String& typeName, String& assemName, String[]& fieldNames, Object[]& fieldValues)


   at System.AppDomain.add_AssemblyLoad(AssemblyLoadEventHandler value)
   at SandboxTest.Sandbox..ctor() in C:\Dev\Projects\Botfield\SandboxTest\Program.cs:line 36
The action that failed was:
Demand
The type of the first permission that failed was:
System.Security.Permissions.ReflectionPermission
The first permission that failed was:
<IPermission class="System.Security.Permissions.ReflectionPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
version="1"
Flags="MemberAccess"/>

The demand was for:
<IPermission class="System.Security.Permissions.ReflectionPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
version="1"
Flags="MemberAccess"/>

The granted set of the failing assembly was:
<PermissionSet class="System.Security.PermissionSet"
version="1">
<IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
version="1"
Flags="Execution"/>
</PermissionSet>

My best guess is that there's some event-subscription code under the hood executing in the new sandboxed AppDomain without the required security permissions, but I don't know how to work around it without giving full reflection-capacity to the sandboxed AppDomain. Does anyone have a suggestion or explanation, please?


Solution

  • Short answer:

    Hidden method for adding handler to event AppDomain.AssemblyLoad - marked by SecurityCriticalAttribute. Check ILASM:

    .method public hidebysig newslot specialname virtual final 
            instance void  add_AssemblyLoad(class System.AssemblyLoadEventHandler 'value') cil managed
    {
      .custom instance void System.Security.SecurityCriticalAttribute::.ctor() = ( 01 00 00 00 ) 
      // Code size       0 (0x0)
    } // end of method AppDomain::add_AssemblyLoad
    

    In order to execute this method you MUST execute it in FullTrust mode (of your sandbox domain). End of story.

    Long answer:

    You are dealing with Cross Domain communication. Means your event handler will be executed in the space of Sandbox domain, and then using remoting enrolled to you parent domain. The code you provided requires Reflection permission, it does not matter what event you use in that approach - security critical or not.

    So if you want your sandbox domain to do safe communication with your parent domain you should use classic .NET Remoting approach, this code will not require any additional permissions and allow to notify parent domain about events happened on sandbox domain:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Security;
    using System.Security.Permissions;
    using System.Diagnostics;
    using System.Reflection;
    
    namespace SandboxTest
    {
        class Program
        {
            static void Main(string[] args)
            {
                Sandbox sandbox = new Sandbox();
                Console.ReadLine();
            }
        }
    
        class Sandbox
        {
            AppDomain domain;
    
            public Sandbox()
            {
                PermissionSet ps = new PermissionSet(PermissionState.None);
                ps.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
    
                try
                {
                    domain = AppDomain.CreateDomain("Sandbox", AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.SetupInformation, ps);
                    var tp = typeof(MyInit);
    
                    var obj = (MyInit)domain.CreateInstanceAndUnwrap(tp.Assembly.FullName, tp.FullName);
                    var myCallBack = new MyCallBack();
                    myCallBack.Generated += new EventHandler(myCallBack_Generated);
                    obj.Subscribe(myCallBack);
                    obj.GenerateCallBackEvent();
                }
                catch (Exception e)
                {
                    Trace.WriteLine(e.ToString());
                    throw e;
                }
            }
    
            void myCallBack_Generated(object sender, EventArgs e)
            {
                //Executed in parent domain
            }
    
    
    
        }
    
    
        public class MyCallBack:MarshalByRefObject
        {
            public void GenerateEvent()
            {
                //Executed in parent domain, but triggered by sandbox domain
                if (Generated != null) Generated(this, null);
            }
    
            //for parent domain only
            public event EventHandler Generated;
        }
    
        public class MyInit:MarshalByRefObject
        {
            public MyInit()
            {
    
            }
            MyCallBack callback;
            public void Subscribe(MyCallBack callback)
            {
                //executed on sandbox domain
                this.callback = callback;
            }
    
            public void GenerateCallBackEvent()
            {
                //executed on sandbox domain
                callback.GenerateEvent();
            }
    
    
        }
    }