Host protection

Jun 17, 2010 at 9:29 PM

I am building a small prototype to enable scripting in my application using IronPython and the DLR. It works great and was incredibly easy to set up.

But I am surprised that I haven't found an answer to my main concern regarding application scripting: how do you protect the host application and computer? I would have thought this was a basic requirement for any scripting solution...

When I say "protect the host" I actually have two different aspects in mind:

1) Users are going to download scripts from the web. I'd like to be sure that a script can't be malicious and harm the users' computer. E.g. a script can directly access the file system, "f = open(...)". That looks too dangerous to me. In my case scripts are only intended to do some computations and manipulate my application through an API defined by myself. (Although - and I know this seems conflicting - being able to import other scripts from the disk would still be nice... as long as its safe. Maybe a callable .NET method could help do that for the script.)

2) I wonder how much of my application a script can access. Since it's loaded into the same AppDomain I suppose a lot, am I right? I would like to only expose a specific API to the script and prevent manipulation of any other class (but I don't really care about the area of the .NET framework scripts use).

I am pretty sure your answers will include using a custom PlatformAdaptationLayer (how to do that efficiently and properly?) and /or creating a second AppDomain (how do you make it safe? what are the implications regarding cross-AppDomain communications, as the goal is to manipulate my application, which is in the first AppDomain?)

Thanks for your help!

Coordinator
Jun 17, 2010 at 9:53 PM

You’re right about the 2nd app domain – you most likely won’t have to use the platform adaptation layer unless you have scripts you want to load from disk. First you need to create the 2nd app domain with limited trust and run all of the code there. There’s some information on how setup the sandboxed app domain over here: http://msdn.microsoft.com/en-us/library/bb763046.aspx

From there you can do ScriptRuntime.Create(domain) to create a script runtime in the remote app domain (or you can use Python.CreateEngine/CreateRuntime overloads which also accept an AppDomain).

For exposing the object model to the remote app domain you can write a public class which subclasses MarshalByRefObject. You can then create that class in your applications app domain and publish it in the scope of the remote app domain. Then any calls that the app makes will go across the app domain boundary into the fully trusted app domain where you can do whatever you want. Any objects which flow across the object model will also need to be serializable or MarshalByRefObject’s as well.

Alternately you can pull things out of the scope using the *AndWrap methods which return ObjectHandles. You can pass those object handles back to the ObjectOperations APIs to work on them in the remote app domain.

From: jdu [mailto:notifications@codeplex.com]
Sent: Thursday, June 17, 2010 2:29 PM
To: Dino Viehland
Subject: Host protection [dlr:216441]

From: jdu

I am building a small prototype to enable scripting in my application using IronPython and the DLR. It works great and was incredibly easy to set up.

But I am surprised that I haven't found an answer to my main concern regarding application scripting: how do you protect the host application and computer? I would have thought this was a basic requirement for any scripting solution...

When I say "protect the host" I actually have two different aspects in mind:

1) Users are going to download scripts from the web. I'd like to be sure that a script can't be malicious and harm the users' computer. E.g. a script can directly access the file system, "f = open(...)". That looks too dangerous to me. In my case scripts are only intended to do some computations and manipulate my application through an API defined by myself. (Although - and I know this seems conflicting - being able to import other scripts from the disk would still be nice... as long as its safe. Maybe a callable .NET method could help do that for the script.)

2) I wonder how much of my application a script can access. Since it's loaded into the same AppDomain I suppose a lot, am I right? I would like to only expose a specific API to the script and prevent manipulation of any other class (but I don't really care about the area of the .NET framework scripts use).

I am pretty sure your answers will include using a custom PlatformAdaptationLayer (how to do that efficiently and properly?) and /or creating a second AppDomain (how do you make it safe? what are the implications regarding cross-AppDomain communications, as the goal is to manipulate my application, which is in the first AppDomain?)

Thanks for your help!

Read the full discussion online.

To add a post to this discussion, reply to this email (dlr@discussions.codeplex.com)

To start a new discussion for this project, email dlr@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe or change your settings on codePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at codeplex.com

Jun 19, 2010 at 10:28 PM
Edited Jun 19, 2010 at 10:31 PM

A separated AppDomain works well for that purpose, indeed. And it was surprisingly easy to setup and test... but then trouble came.

1. Having a second AppDomain was indeed a no brainer. As was restricting its permissions.

2. Have your host assembly being fully trusted in the 2nd AppDomain requires a strong name. Which is relatively easy, except it means all your assembly references have to be fully trusted as well, which may be more annoying if they are not.

This caused me some trouble... and in the end I realized that I didn't need my host being fully trusted in the 2nd domain! Everything happens in my host's own AppDomain :-(

3. On the other hand, you have to put the APTCA attribute on your host assembly to accept incoming calls from transparent script AppDomain. And you have to put it inside your referenced libraries as well, apparently. (don't quite get this one, but I get errors otherwise)

4. Make the host objects exposed in the scripting environment inherit from MarshalByRefObject. This may be annoying if you need another parent class. In my case it's OK, but it took me some time to realize that those class also have to be public. I know it's obvious but it took me a long time to figure out why none of my ("public") properties were available in Python. The Python runtime isn't the same as your host assembly! So it respects your classes being internal...

Now I have 2 major problems left. I'll expose the one related to the isolation in a second AppDomain here, and open another thread for the 2nd one.

.NET uses MarshalByRefObject to pass the shared objects between my host and scripts accross AppDomains (Serializable isn't an option, the instances have to be connected with the backing data in the host). But MarshalByRefObject means remoting, which means Lifetime management. And that's just a big mess. It comes with a default 6 minutes time out, which of course isn't appropriate. Then there are Sponsors, but they are complicated and I'm not even sure how to do this correctly...

Of course, my use-case is simple. A "simple" cross-AppDomain garbage collection is all I need. My host MBR (MarshallByRef) objects should live as long as the script scope holds a reference to them. No less (otherwise when the user uses the console she gets unexplicable errors), and no more (I don't want to leak memory and may be creating many of MBR during scripts execution). If the scope has permanent (globals) references, they should be kept alive until the scope itself is dimissed. That's a simple GC scenario, but apparently the GC doesn't work across AppDomains, even in the same Process.

I should I solve this issue? It's seems to me that no time-based expiration is reasonable. Keeping things alive forever isn't good either because I would be leaking a lot of memory. I suppose the solution is going to involve some kind of Sponsor implementation, but how should that work? How can the sponsor now if the scope still has a reference to the object itself? And what happens to the Sponsor if the scope is discarded? I think the Addin team used the MBR as its own sponsor object, that may be an idea to dig further...

Is there a solution built-in the DLR? This looks like a reasonably common request and the DLR has bult-in support for running in a remote AppDomain, so maybe it has something to help with MBR? I couldn't find anything using Google...

Jun 20, 2010 at 1:30 PM
Edited Jun 20, 2010 at 9:11 PM

More explorations... more questions!

Regarding the Lifetime problem, I thought: hey, IronPython has to do exactly the same thing on its side. After all ScriptScope is living in the remote AppDomain, so how is it kept alive?

It turns out that it returns null for InitializeLifetimeService... doesn't that means it's kept alive forever?

Even when my application throws away the scope, as long as I don't throw away the complete app domain, the scope will survive due to its remoting lease! Basically, I'm leaking memory like crazy, don't I?

What is the DLR team take on this?

[EDIT] And here's one new problem: as soon as I override InitializeLifetimeServices to return null (no expiration), IronPython doesn't see my object properties anymore:
me.name --> returns OK
After overriding InitializeLifetiemServices to return null:
me.name --> 'MarshalByRefObject' has no attribute 'name'

I have the impression that IronPython isn't ready for "safe" (i.e. in a separate partial trust AppDomain) scripting yet. Too many issues keep appearing and there's no answer in sight on the internet :-(

[EDIT 2] Investigation shows that the name lookup failure when overriding InitializeLifetimeServices is due to some kind security demand. If the 2nd app domain is fully trusted, "me.name" works. Can't figure out how to make it work properly, though. The only Security exception I can catch seems to be a full trust request, and that's a big no no...
(Note that InitializeLifetimeServices is marked with SecurityCritical attribute.)

[EDIT 3] After more debugging (it's getting on my nerves...): if I say that my assembly is fully trusted in the partial trust domain, IronPython manages to "see" the "me.name" member on the proxy, even when InitializeLifetimeServices is overriden. But then when I try to access any member, I get an error (Request failed, still some security issues...). Even though the members are marked with SecuritySafeCritical. Notice that this error happens all the time (with InitializeLifetimeServices overriden, or not) when my assembly is fully trusted.

Jun 21, 2010 at 3:58 AM

You are right - we do return null from InitializeLifetimeServices and thus keep that hosting objects alive forever. We haven't had time to implement this yet. It's the host who should be responsible for managing lifetime of these objects so I think all hosting MBROs should just delegate lifetime decisions to the ScriptHost. I'm thinking of making ScriptHost a sponsor for all hosting objects. Whenever a hosting MBRO's TTL is exceeded we'll ask ScriptHost for a renewal. The default behavior would still be an infinite lifetime though you would have a way of changing it. (You can specify a SriptHost for each script runtime you create via setting ScriptRuntimeSetup.HostType property.) Would this work in your scenario?

BTW: You can find basics about remoting and sponsors here: http://msdn.microsoft.com/en-us/magazine/cc300474.aspx.

InitializeLifetimeServices is SecurityCritical method thus all its overrides must be security critical. What security attributes do you have on the host assembly and how do you create the sandbox domain?

 

Jun 21, 2010 at 9:06 AM

Thanks for the information Tomas.

Regarding the sponsor objects. I think there are 2 different issues here, as objects are mashalled both ways:

- Who keeps the ScriptEngine, ScriptScope, etc. alive [Sandbox --> Host application]?
It's definitively the host application but there can be many scenarios. In my specific case it makes sense for the ScriptEngine to live forever as I am going to keep it alive as long as its sandbox (AppDomain). On the other hand, I may throw away a ScriptScope and create a new one. In that case the discarded ScriptScope should be reclaimed by the GC. But to happen my application has to end its lease on this remote object. So I'd need to become a sponsor for the ScriptScope. As you can see, one really needs a way to finely tune lifetime behaviors.
Infinite lifetime plus an API to end the lease is probably the simplest way out, but I don't think this is supported by .NET remoting.

- Who keeps the local objects exposed to Python scripts alive [Host application --> Sandbox]?
Again maximum flexibility is required as Python may not be the only consumer in the app domain. That said, my scenario (probably the most common one) is that scripts are ran in a sandbox and "host application" objects should live in that sandbox as long as the scripts hold a reference to them. With that in mind, ScriptHost may look like a good Sponsor candidate. (Not sure how it would know when all references are lost, though.)

Sadly, in my case all these tricky issues could be very easily solved if the GC could work cross-appdomains in the same process (which in theory is achievable, although it wouldn't solve the case of "true" remoting across processes or even machines).

Thank you for the MSDN article. I read it already and I think I have a solid grasp on how Lifetime and Sponsors work. What really complicates everything here is that the sandbox is a partially trusted AppDomain. And it breaks everything. Maybe you can help because I don't have an in depth knowledge of security in .NET 4 yet.

I have tried almost all possible combinations, I think. Each time a different piece of the puzzle breaks.

Let's say I'd like to make the simplest solution work (although there would be no GC): I'd like my remoted objects to return null for InitializeLifetimeServices, so that they live forever.

1. The sandbox is a partial trust AppDomain. Let's give it Execution and Reflection permissions.
2. The application assembly doesn't have any security attribute. I've tried the good old AllowPartiallyTrustedCallersAttribute but it creates mostly problems (can't start the application anymore. Apparently any "serious" call from the application when bearing this attribute fails).
3. The remoted objects are marked as SecuritySafeCritical. At that point everything works as expected (great!)
4. If I want to extend the lifetime I have to override InitializeLifetimeServices. Easy to do. The override has to be tagged with SecurityCritical, of course.
The problem is that after step 4, my attributes aren't discovered by Python anymore. It considers that my objects are plain MBRO rather than their own class.
5. I can make my host assembly fully trusted in the sandbox -- or not. My experience is that:
  5a Not fully trusted. The calls to attributes of my objects works, but after overriding a SecurityCritical method Python can't discover its attributes anymore.
  5a. Fully trusted makes attributes on my objects discoverable, but any call fails (can't say why exactly).
My conclusion is that Python has trouble loading my class definition once it contains a SecurityCritical method and isn't in a fully trusted assembly. But the problem is that when my object is in a fully trusted assembly all calls are broken and I have no idea why.

This is really driving me nuts. At this point I would accept any hack or solution to make my objects live longer in the sandbox. (And I've tried a lot of variations with dummy Sponsors... but in partial trust there's a lot of things you can't do.)

Jun 21, 2010 at 6:45 PM

The reason why Python doesn't see the members seems to be that Remoting fails to load the type of your MBRO into the sandbox. Object.GetType in that case returns typeof(MarshalByRefObject), which indeed doesn't have any of the members you define. You can make this work by adding the assembly that defines your MBROs into the set of fully-trusted assemblies in the sandbox. I have a working example that creates the sandbox as follows:

        public void Remoting() {
            AppDomainSetup setup = new AppDomainSetup();
            Assembly thisAssembly = Assembly.GetExecutingAssembly();
            setup.ApplicationBase = Path.GetDirectoryName(thisAssembly.Location);

            AssemblyName name = typeof(MBRO1).Assembly.GetName();
            StrongName sn = new StrongName(
                new StrongNamePublicKeyBlob(name.GetPublicKey()),
                name.Name,
                name.Version
            );

            PermissionSet pset = new PermissionSet(PermissionState.None);
            pset.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
            pset.AddPermission(new ReflectionPermission(PermissionState.Unrestricted));
            
            setup.PartialTrustVisibleAssemblies = new[] { sn.Name + ", PublicKey=" + sn.PublicKey.ToString() };
            AppDomain domain = AppDomain.CreateDomain("Sandbox", AppDomain.CurrentDomain.Evidence, setup, pset, sn);

            var c = new MBRO1();

            ScriptRuntimeSetup srs = new ScriptRuntimeSetup();
            srs.AddRubySetup();
            var runtime = ScriptRuntime.CreateRemote(domain, srs);
            var engine = runtime.GetEngine("rb");
            runtime.Globals.SetVariable("C", c);
            engine.Execute("puts C.foo");
        }

        public class MBRO1 : MarshalByRefObject {
            [SecurityCritical]
            public override object InitializeLifetimeService() {
                return null;
            }

            public int Foo() {
                return 123;
            }
        }

 The attribute on the host's assembly is an explicit APTCA - this means that the assembly is APTCA only if it is added to PartialTrustVisibleAssemblies list:

<font face="Consolas" size="2">

[

</font>

assembly: AllowPartiallyTrustedCallers(PartialTrustVisibilityLevel = PartialTrustVisibilityLevel

.NotVisibleByDefault)]



Does this work for you?

Jun 22, 2010 at 4:54 PM

Thanks for keeping up with me on this one.

In the meantime I have found my own solution to my problem. I have created a Sponsor object in the host AppDomain and registered it there. It now keeps my MRBO alive as long as I want them to (with the added benefit that I can tear them down if I want to).

I'm sorry but I don't have the courage (at least now) to sign all my assemblies again to test if using the PartialTrustVisibilityLevel attribute would solve the issue. I think it may but it has gotten so much on my nerves that I don't want to dig into that again now that I have a working (although wacky) solution. And as I said the Sponsor is better in that regard, that it allows ending the object lifetime when it's not needed anymore. It's my understanding that a MBRO with unlimited lifetime would live as long as its AppDomain (basically forever for the host side).

My very frustrating experience with all this is that I'm not sure the technology is ready. At least good documented examples are lacking. Microsoft really ought to publish a good end-to-end example, with custom Python scripts executed in partial trust, including some remoted objects to create an interface between the host and the scripts, and good lifetime/GC management.

I have to say that I've grown used to a far better experience with the .NET ecosystem. The way every fix to a frustrating issue leads to another issue was really remniscent of some bad java and PHP projects I had done in the past.

That said, everything is not black. I'd like to take this opportunity to say that the DLR, Scripting APIs and IronPython are really awesome tools. During my first tests I ran Python in the same AppDomain and I had a functionnal interactive console up in no time. Executing scripts requires very little effort, performance is great, and the result (an interactive application) is just awesome and boasts a whole lot of new possibilities. Thanks for making that possible! Just have a closer look at the "security"... All my problems came form a simple requirement: make that console I had created safe for running untrusted scripts.

Coordinator
Jun 22, 2010 at 5:15 PM

Jdu, totally get the frustration, and very sorry you had to bush whack with machete in hand to get through it. I might defensively say you're treading on more advanced ground where our two devs have spent less time due to IRb and IPy work, but you're absolutely right that we should work toward a decent and documented end-to-end sample with remoting and lifetime management involved. This is a scenario we believe in supporting. As we go forward on the hosting APIs, I'll keep a remoting sample on the list of things to get done.

Otherwise, thanks for the kind words in light of your experience!!

Cheers,

Bill

From: jdu [mailto:notifications@codeplex.com]
Sent: Tuesday, June 22, 2010 9:55 AM
To: Bill Chiles
Subject: Re: Host protection [dlr:216441]

From: jdu

Thanks for keeping up with me on this one.

In the meantime I have found my own solution to my problem. I have created a Sponsor object in the host AppDomain and registered it there. It now keeps my MRBO alive as long as I want them to (with the added benefit that I can tear them down if I want to).

I'm sorry but I don't have the courage (at least now) to sign all my assemblies again to test if using the PartialTrustVisibilityLevel attribute would solve the issue. I think it may but it has gotten so much on my nerves that I don't want to dig into that again now that I have a working (although wacky) solution. And as I said the Sponsor is better in that regard, that it allows ending the object lifetime when it's not needed anymore. It's my understanding that a MBRO with unlimited lifetime would live as long as its AppDomain (basically forever for the host side).

My very frustrating experience with all this is that I'm not sure the technology is ready. At least good documented examples are lacking. Microsoft really ought to publish a good end-to-end example, with custom Python scripts executed in partial trust, including some remoted objects to create an interface between the host and the scripts, and good lifetime/GC management.

I have to say that I've grown used to a far better experience with the .NET ecosystem. The way every fix to a frustrating issue leads to another issue was really remniscent of some bad java and PHP projects I had done in the past.

That said, everything is not black. I'd like to take this opportunity to say that the DLR, Scripting APIs and IronPython are really awesome tools. During my first tests I ran Python in the same AppDomain and I had a functionnal interactive console up in no time. Executing scripts requires very little effort, performance is great, and the result (an interactive application) is just awesome and boasts a whole lot of new possibilities. Thanks for making that possible! Just have a closer look at the "security"... All my problems came form a simple requirement: make that console I had created safe for running untrusted scripts.

Read the full discussion online.

To add a post to this discussion, reply to this email (dlr@discussions.codeplex.com)

To start a new discussion for this project, email dlr@discussions.codeplex.com

You are receiving this email because you subscribed to this discussion on CodePlex. You can unsubscribe or change your settings on codePlex.com.

Please note: Images and attachments will be removed from emails. Any posts to this discussion will also be available online at codeplex.com