Search code examples
.netf#clrsandboxappdomain

Should ApplicationBase be different for the sandbox AppDomain?


What are the exact security implications of setting ApplicationBase of a slave sandbox domain to the same path as the hosting domain?

I found MSDN guidelines that state that ApplicationBase should be different for the slave domain "If the ApplicationBase settings are the same, the partial-trust application can get the hosting application to load (as fully trusted) an exception it defines, thus exploiting it" (p. 3):

http://msdn.microsoft.com/en-us/library/bb763046.aspx

How exactly would this exploit work?

In my scenario I am willing to run all assemblies located under the ApplicationBase in full trust. I am sandboxing the slave AppDomain exclusively to limit the rights of dynamically generated assemblies within that domain. I tried following the guidelines but changing the ApplicationBase property seems to break a bi-directional communication bridge I have between the domains, due to an assembly loading into LoadFrom context, so I would like to avoid it.

Sample F# code demonstrating the problem with distinct ApplicationBase values:

module Main =

    open System
    open System.Diagnostics
    open System.IO
    open System.Reflection
    open System.Security
    open System.Security.Permissions
    open System.Security.Policy

    /// Change this switch to observe the problem.
    let useSameApplicationBase = true

    let getStrongName (a: Assembly) =
        match a.Evidence.GetHostEvidence<StrongName>() with
        | null -> None
        | sn -> Some sn

    let getAssemblies () =
        [|
            Assembly.GetExecutingAssembly()
        |]

    let buildAppDomain () =
        let fullTrust =
            getAssemblies ()
            |> Array.choose getStrongName
        let evidence = null
        let appBase =
            if useSameApplicationBase then
                AppDomain.CurrentDomain.BaseDirectory
            else
                Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Untrusted")
        let setup = AppDomainSetup(ApplicationBase = appBase)
        let perms = PermissionSet(PermissionState.None)
        AppDomain.CreateDomain("SLAVE", null, setup, perms, fullTrust)

    [<Sealed>]
    type Backer() =
        inherit MarshalByRefObject()
        member this.Pong() =
            Console.WriteLine("PONG IN DOMAIN = {0}", AppDomain.CurrentDomain.FriendlyName)

    [<Sealed>]
    type Sandbox() =
        inherit MarshalByRefObject()
        member this.Start(backer: obj) =
            Console.WriteLine("RUN IN SLAVE DOMAIN = {0}", AppDomain.CurrentDomain.FriendlyName)
            (backer :?> Backer).Pong()

    let test () =
        let dom = buildAppDomain ()
        try
            let handle =
                Activator.CreateInstanceFrom(dom,
                    typeof<Sandbox>.Assembly.Location,
                    typeof<Sandbox>.FullName)
            let sandbox = handle.Unwrap() :?> Sandbox
            sandbox.Start(Backer())
        finally
            AppDomain.Unload(dom)

    test ()

Solution

  • module Main =
    
        open System
        open System.Diagnostics
        open System.IO
        open System.Reflection
        open System.Security
        open System.Security.Permissions
        open System.Security.Policy
    
        /// Change this switch to observe the problem.
        let useSameApplicationBase = false
    
        let getStrongName (a: Assembly) =
            match a.Evidence.GetHostEvidence<StrongName>() with
            | null -> None
            | sn -> Some sn
    
        let getAssemblies () =
            [|
                Assembly.GetExecutingAssembly()
            |]
    
        let buildAppDomain () =
            let fullTrust =
                getAssemblies ()
                |> Array.choose getStrongName
            let evidence = null
            let appBase =
                if useSameApplicationBase then
                    AppDomain.CurrentDomain.BaseDirectory
                else
                    Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Untrusted")
            let setup = AppDomainSetup(ApplicationBase = appBase)
            let perms = PermissionSet(PermissionState.None)
            AppDomain.CreateDomain("SLAVE", null, setup, perms, fullTrust)
    
        module AssemblyResolveSetup = 
            let install() = 
                let resolveHandler = 
                        ResolveEventHandler(
                            fun _ args ->
                                // try to find requested assembly in current domain
                                let name = AssemblyName(args.Name)
                                let asmOpt =
                                    AppDomain.CurrentDomain.GetAssemblies()
                                    |> Array.tryFind(fun asm -> AssemblyName.ReferenceMatchesDefinition(AssemblyName(asm.FullName), name))
                                defaultArg asmOpt null
                        )        
                AppDomain.CurrentDomain.add_AssemblyResolve(resolveHandler)
    
        [<Sealed>]
        type Backer() =
            inherit MarshalByRefObject()
            member this.Pong() =
                Console.WriteLine("PONG IN DOMAIN = {0}", AppDomain.CurrentDomain.FriendlyName)
    
        [<Sealed>]
        type Sandbox() =
            inherit MarshalByRefObject()
            do AssemblyResolveSetup.install()
            member this.Start(backer: obj) =
                Console.WriteLine("RUN IN SLAVE DOMAIN = {0}", AppDomain.CurrentDomain.FriendlyName)
                (backer :?> Backer).Pong()
    
        let test () =
            let dom = buildAppDomain ()
            try
                let handle =
                    Activator.CreateInstanceFrom(dom,
                        typeof<Sandbox>.Assembly.Location,
                        typeof<Sandbox>.FullName)
                let sandbox = handle.Unwrap() :?> Sandbox
                sandbox.Start(Backer())
            finally
                AppDomain.Unload(dom)
    
        test ()
    

    UPDATE (Assuming that test code is contained in assembly Sandbox.exe)

    Q:how resolution finds the assembly in SLAVE by looking in SLAVE (CurrentDomain), sounds like a vicious circle

    SLAVE domain already contains Sandbox.exe but it is loaded in LoadFrom context so it won't be probed automatically when resolving dependencies for Load context (Choosing a binding context).

    Q: why it breaks for asm.GetName() instead of AssemblyName(asm.FullName)

    Assembly.GetName requires FileIOPermission, Assembly.FullName - don't so I think if you replace

    AssemblyName(asm.FullName)
    

    with

    let name = AssemblyName(args.Name)
    let p = new FileIOPermission(PermissionState.Unrestricted)
    p.Assert()
    try
        let asmOpt =
            AppDomain.CurrentDomain.GetAssemblies()
            |> Array.tryFind(fun asm -> AssemblyName.ReferenceMatchesDefinition(asm.GetName(), name))
        defaultArg asmOpt null
    finally
        CodeAccessPermission.RevertAssert()
    

    this should also work (haven't tried)

    Q: why it brakes for static do AssemblyResolveSetup.install()

    this is the only F# specific issue here. I guess your test project is single file project that is compiled to exe. Per F# spec:

    For executable files that have an implicit entry point, the static initializer for the last file that appears on the command line is the body of the implicit entry point function.

    So code in 'static do' block will be placed before the call to 'test()' in the implicit entry point instead of being compiled to the body of static constructor. Fix - either put Test module to the separate non-last file or move it to the library