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 ()
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