Search code examples
c#reflectionappdomain

SecurityException when executing reflected method in sandbox AppDomain


I am using a separate AppDomain as a sandbox, and trying to execute a method that is constructed via reflection.

When the method is invoked, a

SecurityException

...is thrown, even though the sandbox AppDomain has ReflectionPermission(PermissionState.Unrestricted) set on its PermissionSet.

The invocation does work when the PermissionSet is set to PermissionState.Unrestricted, but that defeats the purpose of a sandbox.

Here's an example that demonstrates the issue:

using System;
using System.Security;
using System.Security.Permissions;

namespace ConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();
            var program = new Program();

            var customDomain = program.CreateDomain();
            var result = program.Execute(customDomain, (x) =>
            {
                var type = x.GetType();
                var propertyInfo = type.GetProperty("Name");
                var method = propertyInfo.GetMethod;
                var res = method.Invoke(x, null) as string;
                return res;
            }, person);
            Console.WriteLine(result);
            Console.ReadLine();
        }

        public object Execute(AppDomain domain, Func<object, object> toExecute, params object[] parameters)
        {
            var proxy = new Proxy(toExecute, parameters);
            var result = proxy.Invoke(domain);
            return result;
        }

        private AppDomain CreateDomain()
        {
            var appDomainSetup = new AppDomainSetup()
            {
                ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
                ApplicationName = "UntrustedAppDomain"
            };

            // Set up permissions
            var permissionSet = new PermissionSet(PermissionState.None);
            permissionSet.AddPermission(new SecurityPermission(PermissionState.Unrestricted));
            permissionSet.AddPermission(new ReflectionPermission(PermissionState.Unrestricted));

            // Create the app domain.
            return AppDomain.CreateDomain("UntrustedAppDomain", null, appDomainSetup, permissionSet);
        }

        private sealed class Proxy : MarshalByRefObject
        {
            private Delegate method;
            private object[] args;
            private object result;

            public Proxy(Delegate method, params object[] parameters)
            {
                this.method = method;
                this.args = parameters;
            }

            public object Invoke(AppDomain customDomain)
            {
                customDomain.DoCallBack(Execute);
                return this.result;
            }

            private void Execute()
            {
                this.result = this.method.DynamicInvoke(this.args);
            }
        }
    }

    public class Person
    {
        public Person()
        {
            this.Name = "Test Person";
        }

        public string Name { get; set; }
    }
}

Solution


  • This is an addition to my comment above


    SecurityException when executing reflected method in sandbox AppDomain

    You are attempting to execute a method on an object that was created in the primary AppDomain from the context in the secondary domain. In other words, code in the sandbox is trying to call code in the primary domain which is not allowed. When using sandboxes via AppDomains, Proxy must be created in the sandbox domain via CreateInstanceAndUnwrap from the primary app domain.

    Change this:

    public object Execute(AppDomain domain, Func<object, object> toExecute, params object[] parameters)
        {
            var proxy = new Proxy(toExecute, parameters);
            var result = proxy.Invoke(domain);
            return result;
        }
    

    ...to:

        public object Execute(AppDomain domain, Func<object, object> toExecute, params object[] parameters)
        {
            var t = typeof(Proxy); // add me
            var args = new object[] {toExecute, parameters};
            var proxy = domain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName, false,
                BindingFlags.Default, 
                null,
                args,
                null,
                null) as Proxy; // add me
    
            //var proxy = new Proxy(toExecute, parameters);
            var result = proxy.Invoke(domain);
            return result;
        }
    

    ...and make Person either [Serializable] or derive from MarshalByRefObject depending on whether you want to transfer copies to or pass indirectly modifyable objects to the sandbox respectively.

    Thank you for your reply. The above sample code can work if the Proxy is created in the sandbox domain, and the Person class is Serializable, but then the person object used in the secondary domain is indeed not the same instance as the one in the primary domain - it gets serialized in the CreateInstanceAndUnwrap method

    That's correct and is by design. Note your Person as shown in your post isn't marked as Serializable so that would have resulted in an error. I'm assuming you fixed that. If you want the "same" object to be passed about have the class derive from MarshalByRefObject.

    Here is the complete code:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Security;
    using System.Security.Permissions;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace Sandboxes1
    {
        class Program
        {
            static void Main(string[] args)
            {
    
    
                var person = new Person();
                Console.WriteLine("[{0}] Person's current name: {1}", AppDomain.CurrentDomain.FriendlyName, person.Name);
    
                var program = new Program();
    
                var customDomain = program.CreateDomain();
                var result = program.Execute(customDomain, (x) =>
                {
                    Console.WriteLine("[{0}] Inside delegate", AppDomain.CurrentDomain.FriendlyName);
                    var type = x.GetType();
                    var propertyInfo = type.GetProperty("Name");
                    var method = propertyInfo.GetMethod;
                    var res = method.Invoke(x, null) as string;
    
                    dynamic d = x;
                    d.Name = "Fozzy Bear";
                    Console.WriteLine("[{0}] delegate changed person's name to- {1}", AppDomain.CurrentDomain.FriendlyName, d.Name);
    
                    return res;
                }, person);
                Console.WriteLine("[{0}] Result: {1}", AppDomain.CurrentDomain.FriendlyName, result);
                Console.WriteLine("[{0}] Person's current name: {1}", AppDomain.CurrentDomain.FriendlyName, person.Name);
                Console.ReadLine();
            }
    
            public object Execute(AppDomain domain, Func<object, object> toExecute, params object[] parameters)
            {
                var t = typeof(Proxy); // add me
                var args = new object[] {toExecute, parameters};
                var proxy = domain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName, false,
                    BindingFlags.Default, 
                    null,
                    args,
                    null,
                    null) as Proxy; // add me
    
                //var proxy = new Proxy(toExecute, parameters);
                var result = proxy.Invoke(domain);
                return result;
            }
    
            private AppDomain CreateDomain()
            {
                var appDomainSetup = new AppDomainSetup()
                {
                    ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
                    ApplicationName = "UntrustedAppDomain"
                };
    
                // Set up permissions
                var permissionSet = new PermissionSet(PermissionState.None);
                permissionSet.AddPermission(new SecurityPermission(PermissionState.Unrestricted));
                permissionSet.AddPermission(new ReflectionPermission(PermissionState.Unrestricted));
    
                // Create the app domain.
                return AppDomain.CreateDomain("UntrustedAppDomain", null, appDomainSetup, permissionSet);
            }
    
            private sealed class Proxy : MarshalByRefObject
            {
                private Delegate method;
                private object[] args;
                private object result;
    
                public Proxy(Delegate method, params object[] parameters)
                {
                    Console.WriteLine("[{0}] Proxy()", AppDomain.CurrentDomain.FriendlyName);
                    this.method = method;
                    this.args = parameters;
                }
    
                public object Invoke(AppDomain customDomain)
                {
                    Console.WriteLine("[{0}] Invoke()", AppDomain.CurrentDomain.FriendlyName);
    
                    customDomain.DoCallBack(Execute);
                    return this.result;
                }
    
                private void Execute()
                {
                    Console.WriteLine("[{0}] Execute()", AppDomain.CurrentDomain.FriendlyName);
    
                    this.result = this.method.DynamicInvoke(this.args);
                }
            }
        }
    
        [Serializable]
        public class Person
        {
            private string _name;
    
            public Person()
            {
                Name = "Test Person";
            }
    
            public string Name
            {
                get
                {
                    Console.WriteLine("[{0}] Person.getName()", AppDomain.CurrentDomain.FriendlyName);
                    return _name;
                }
                set { _name = value; }
            }
        }
    }
    

    ...which produces the following output:

    enter image description here

    Security Risks

    It is up to your design whether you want code in a sandbox to modify objects from another appdomain. In my opinion that is a security risk and arguably negates the purpose of sandboxes. So if you must pass inter-domain objects about, pass them as Serializable where copies of objects are passed about.

    Better Security?

    The approach I tend to use in my code is to:

    1. define a Manager class that creates a secondary AppDomain
    2. Manager uses CreateInstanceAndUnwrap to create a MarshalByRefObject Proxy class in the secondary AppDomain
    3. Manager calls into Proxy to say LoadPlugins(). This means that any plug-in created objects now automatically exist in the secondary AppDomain
    4. Ensure that any method of the Proxy's methods that are called from the primary AppDomain are enclosed in try-catch. Do not let any exception bubble up to the primary AppDomain because it's quite possible that dodgy code from a dubious plugin could throw a custom exception that, when finalising can execute any code it wishes with full permissions due to the call stack now being on the primary AppDomain. This is one of the few occassions in .NET programming where you must catch everything and don't throw
    5. If the primary AppDomain needs to talk to an object in the secondary appdomain, do so via calling your Proxy and have it call the method on your behalf. Again ensure that this proxy method is fully wrapped in a try-catch all

    Conclusion

    I wasn't aware of AppDomain.DoCallBack and now that I see it I don't like it. There is too much risk in the delegate accidentally using objects it shouldn't particularly where the delegate code is defined in a method that will be a mixture of some code executed by one domain; other parts by another domain.

    More Info

    • There was a brilliant MSDN Magazine article "Add-ins - do you trust it" (or some such) on AppDomains circa 2006 from which I have based my answer on. Sadly Microsoft have taken the HTML form offline and only have .chm files which are difficult to search.