Procedura: eseguire codice parzialmente attendibile in un oggetto sandbox
L'utilizzo di sandbox consiste nell'esecuzione di codice in un ambiente di sicurezza soggetto a restrizioni, per limitare le autorizzazioni di accesso concesse al codice. Ad esempio, se si dispone di una libreria gestita di un'origine la cui attendibilità non è sicura, è opportuno non eseguirla come completamente attendibile. È invece opportuno inserire il codice in una sandbox per limitare le autorizzazioni concedendo solo quelle necessarie, ad esempio l'autorizzazione Execution.
È inoltre possibile utilizzare la modalità sandbox per eseguire test del codice da distribuire che verrà eseguito in ambienti parzialmente attendibili.
Un modo efficace per fornire una sandbox per le applicazioni gestite è costituito da AppDomain. I domini dell'applicazione utilizzati per l'esecuzione di codice parzialmente attendibile dispongono di autorizzazioni che definiscono le risorse protette disponibili per l'esecuzione all'interno di AppDomain. Il codice che viene eseguito in AppDomain è vincolato dalle autorizzazioni associate a AppDomain ed è autorizzato ad accedere solo alle risorse specificate. AppDomain include inoltre una matrice StrongName utilizzata per identificare gli assembly che devono essere caricati come completamente attendibili. In questo modo l'autore di un oggetto AppDomain può avviare un nuovo dominio creato mediante sandbox che consente agli assembly di supporto specifici di essere completamente attendibile. Un'altra opzione per caricare gli assembly come completamente attendibili consiste nel collocarli nella Global Assembly Cache, benché in questo modo gli assembly vengano caricati come completamente attendibili in tutti i domini applicazione creati nel computer specificato. L'elenco di nomi sicuri supporta la decisione per AppDomain che garantisce definizioni più restrittive.
È possibile utilizzare l'overload del metodo AppDomain.CreateDomain(String, Evidence, AppDomainSetup, PermissionSet, StrongName[]) per specificare il set di autorizzazioni per le applicazioni in esecuzione in una sandbox. L'overload consente di specificare il livello esatto di sicurezza dall'accesso di codice desiderato. Gli assembly caricati in un oggetto AppDomain tramite questo overload possono avere unicamente il set di autorizzazioni specificato o essere completamente attendibili. All'assembly viene concessa l'attendibilità totale se è inserito nella Global Assembly Cache o elencato nel parametro della matrice di fullTrustAssemblies (StrongName). Solo gli assembly completamente attendibili devono essere aggiunti all'elenco di fullTrustAssemblies.
L'overload presenta la firma riportata di seguito:
AppDomain.CreateDomain( string friendlyName,
Evidence securityInfo,
AppDomainSetup info,
PermissionSet grantSet,
params StrongName[] fullTrustAssemblies);
I parametri relativi all'overload del metodo CreateDomain(String, Evidence, AppDomainSetup, PermissionSet, StrongName[]) specificano il nome dell'oggetto AppDomain, l'evidenza per ll'oggetto AppDomain, l'oggetto AppDomainSetup che identifica la base dell'applicazione per la sandbox, il set di autorizzazioni da utilizzare e i nomi sicuri per gli assembly completamente attendibili.
Per motivi di sicurezza, la base dell'applicazione specificata nel parametro info non deve coincidere con la base dell'applicazione host.
Per il parametro grantSet è possibile specificare un set di autorizzazioni creato in modo esplicito oppure un set di autorizzazioni standard creato dal metodo GetStandardSandbox.
A differenza della maggior parte dei caricamenti AppDomain, l'evidenza per l'oggetto AppDomain (fornita dal parametro securityInfo) non viene utilizzata per determinare il set di concessioni per gli assembly parzialmente attendibili. Quest'ultimo viene invece specificato indipendentemente dal parametro grantSet. L'evidenza può tuttavia essere utilizzata per altri scopi, ad esempio per determinare l'ambito dello spazio di memorizzazione isolato.
Per eseguire un'applicazione in una sandbox
Creare il set di autorizzazioni da concedere all'applicazione non attendibile. L'autorizzazione minima che è possibile concedere è Execution. È inoltre possibile concedere autorizzazioni aggiuntive che si ritengono sicure per il codice non attendibile, ad esempio IsolatedStorageFilePermission. Nel codice seguente viene creato un nuovo set di autorizzazioni che contiene solo l'autorizzazione Execution.
PermissionSet permSet = new PermissionSet(PermissionState.None); permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
In alternativa, è possibile utilizzare un set di autorizzazioni denominato esistente, ad esempio Internet.
Evidence ev = new Evidence(); ev.AddHostEvidence(new Zone(SecurityZone.Internet)); PermissionSet internetPS = SecurityManager.GetStandardSandbox(ev);
Il metodo GetStandardSandbox restituisce un set di autorizzazioni Internet o un set di autorizzazioni LocalIntranet a seconda della zona nell'evidenza. Mediante il metodo GetStandardSandbox vengono inoltre costruite autorizzazioni di identità per alcuni oggetti evidenza passati come riferimenti.
Firmare l'assembly che contiene la classe di hosting (denominato Sandboxer in questo esempio) che chiama il codice non attendibile. Aggiungere l'oggetto StrongName utilizzato per firmare l'assembly alla matrice StrongName del parametro fullTrustAssemblies della chiamata a CreateDomain. La classe di hosting deve essere eseguita come completamente attendibile per consentire l'esecuzione del codice parzialmente attendibile o offrire servizi all'applicazione parzialmente attendibile. L'oggetto StrongName di un assembly viene letto nel modo seguente:
StrongName fullTrustAssembly = typeof(Sandboxer).Assembly.Evidence.GetHostEvidence<StrongName>();
Non è necessario aggiungere gli assembly di .NET Framework, ad esempio mscorlib e System.dll, all'elenco di attendibilità totale perché sono caricati come completamente attendibili dalla Global Assembly Cache.
Inizializzare il parametro AppDomainSetup del metodo CreateDomain. Con questo parametro, è possibile controllare molte delle impostazioni del nuovo oggetto AppDomain. La proprietà ApplicationBase è un'impostazione importante e deve essere diversa dalla proprietà ApplicationBase per l'oggetto AppDomain dell'applicazione host. Se le impostazioni ApplicationBase sono uguali, l'applicazione parzialmente attendibile può fare in modo che un'eccezione definita dall'applicazione stessa venga caricata come completamente attendibile dall'applicazione host, consentendo l'exploit. Anche per questo motivo, non è consigliabile un'eccezione catch. Per attenuare i rischi di exploit, impostare la base dell'applicazione host in modo diverso dalla base dell'applicazione creata mediante sandbox.
AppDomainSetup adSetup = new AppDomainSetup(); adSetup.ApplicationBase = Path.GetFullPath(pathToUntrusted);
Chiamare l'overload del metodo CreateDomain(String, Evidence, AppDomainSetup, PermissionSet, StrongName[]) per creare il dominio applicazione utilizzando i parametri specificati.
La firma per questo metodo è la seguente:
public static AppDomain CreateDomain(string friendlyName, Evidence securityInfo, AppDomainSetup info, PermissionSet grantSet, params StrongName[] fullTrustAssemblies)
Informazioni aggiuntive:
Si tratta dell'unico overload del metodo CreateDomain che accetta PermissionSet come parametro e, pertanto, dell'unico overload che consente di caricare un'applicazione in un'impostazione parzialmente attendibile.
Il parametro evidence non viene utilizzato per calcolare un set di autorizzazioni, ma per l'identificazione da parte di altre funzionalità di .NET Framework.
L'impostazione della proprietà ApplicationBase del parametro info è obbligatoria per questo overload.
Il parametro fullTrustAssemblies dispone della parola chiave params, pertanto non è necessario creare una matrice StrongName. È consentito il passaggio di 0, 1 o nomi più sicuri come parametri.
Il codice per la creazione del dominio applicazione è il seguente:
AppDomain newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, fullTrustAssembly);
Caricare il codice nell'oggetto AppDomain sandbox creato. Questa operazione può essere eseguita in due modi:
Chiamare il metodo ExecuteAssembly per l'assembly.
Utilizzare il metodo CreateInstanceFrom per creare un'istanza di una classe derivata da MarshalByRefObject nel nuovo oggetto AppDomain.
Il secondo metodo è preferibile, perché semplifica il passaggio di parametri alla nuova istanza di AppDomain. Il metodo CreateInstanceFrom fornisce due importanti funzionalità:
È possibile utilizzare una codebase che punta a un percorso che non contiene l'assembly.
È possibile effettuare la creazione in Assert per l'attendibilità totale (PermissionState.Unrestricted), che consente di creare un'istanza di una classe critica. Ciò accade ogni volta che un assembly non ha contrassegni di trasparenza e viene caricato come completamente attendibile. Prestare pertanto attenzione per essere certi di creare solo codice attendibile con questa funzione. È consigliabile creare solo istanze di classi completamente attendibili nel nuovo dominio applicazione.
ObjectHandle handle = Activator.CreateInstanceFrom( newDomain, typeof(Sandboxer).Assembly.ManifestModule.FullyQualifiedName, typeof(Sandboxer).FullName );
Si noti che allo scopo di creare un'istanza di una classe in un nuovo dominio, la classe deve estendere la classe MarshalByRefObject
class Sandboxer:MarshalByRefObject
Annullare il wrapping della nuova istanza di dominio in un riferimento in questo dominio. Questo riferimento viene utilizzato per eseguire il codice non attendibile.
Sandboxer newDomainInstance = (Sandboxer) handle.Unwrap();
Chiamare il metodo ExecuteUntrustedCode nell'istanza della classe Sandboxer appena creata.
newDomainInstance.ExecuteUntrustedCode(untrustedAssembly, untrustedClass, entryPoint, parameters);
Questa chiamata viene eseguita nel dominio applicazione creato mediante sandbox che dispone di autorizzazioni limitate.
public void ExecuteUntrustedCode(string assemblyName, string typeName, string entryPoint, Object[] parameters) { //Load the MethodInfo for a method in the new assembly. This might be a method you know, or //you can use Assembly.EntryPoint to get to the entry point in an executable. MethodInfo target = Assembly.Load(assemblyName).GetType(typeName).GetMethod(entryPoint); try { // Invoke the method. target.Invoke(null, parameters); } catch (Exception ex) { //When information is obtained from a SecurityException extra information is provided if it is //accessed in full-trust. (new PermissionSet(PermissionState.Unrestricted)).Assert(); Console.WriteLine("SecurityException caught:\n{0}", ex.ToString()); CodeAccessPermission.RevertAssert(); Console.ReadLine(); } }
System.Reflection viene utilizzato per ottenere un handle di un metodo nell'assembly parzialmente attendibile. L'handle può essere utilizzato per eseguire codice in modo sicuro con autorizzazioni minime.
Nel codice precedente, si noti Assert per l'autorizzazione di attendibilità totale prima di stampare SecurityException.
new PermissionSet(PermissionState.Unrestricted)).Assert()
L'asserzione di attendibilità totale viene utilizzata per ottenere informazioni estese da SecurityException. Senza Assert, tramite il metodo ToString di SecurityException verrà individuata la presenza di codice parzialmente attendibile nello stack e verranno limitate le informazioni restituite. Questa situazione potrebbe causare problemi di sicurezza se il codice parzialmente attendibile potesse leggere tali informazioni, ma questo rischio viene attenuato evitando di concedere UIPermission. L'asserzione di attendibilità totale deve essere utilizzata con cautela, solo quando si ha la certezza di non concedere l'attendibilità totale a codice parzialmente attendibile. Di norma, non chiamare codice non attendibile nella stessa funzione e dopo avere chiamato un'asserzione per attendibilità totale. È consigliabile ripristinare sempre l'asserzione dopo averla utilizzata.
Esempio
Nell'esempio seguente viene implementata la procedura illustrata nella sezione precedente. Nell'esempio un progetto denominato Sandboxer in una soluzione di Visual Studio contiene anche un progetto denominato UntrustedCode, che implementa la classe UntrustedClass. In questo scenario si presuppone che sia stato scaricato un assembly di librerie contenente un metodo che restituisce true o false per indicare se il numero fornito è un numero di Fibonacci. Il metodo tenta invece di leggere un file dal computer. Nell'esempio seguente viene illustrato il codice non attendibile.
using System;
using System.IO;
namespace UntrustedCode
{
public class UntrustedClass
{
// Pretend to be a method checking if a number is a Fibonacci
// but which actually attempts to read a file.
public static bool IsFibonacci(int number)
{
File.ReadAllText("C:\\Temp\\file.txt");
return false;
}
}
}
Nell'esempio seguente viene illustrato il codice dell'applicazione Sandboxer che esegue il codice non attendibile.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Security;
using System.Security.Policy;
using System.Security.Permissions;
using System.Reflection;
using System.Runtime.Remoting;
//The Sandboxer class needs to derive from MarshalByRefObject so that we can create it in another
// AppDomain and refer to it from the default AppDomain.
class Sandboxer : MarshalByRefObject
{
const string pathToUntrusted = @"..\..\..\UntrustedCode\bin\Debug";
const string untrustedAssembly = "UntrustedCode";
const string untrustedClass = "UntrustedCode.UntrustedClass";
const string entryPoint = "IsFibonacci";
private static Object[] parameters = { 45 };
static void Main()
{
//Setting the AppDomainSetup. It is very important to set the ApplicationBase to a folder
//other than the one in which the sandboxer resides.
AppDomainSetup adSetup = new AppDomainSetup();
adSetup.ApplicationBase = Path.GetFullPath(pathToUntrusted);
//Setting the permissions for the AppDomain. We give the permission to execute and to
//read/discover the location where the untrusted code is loaded.
PermissionSet permSet = new PermissionSet(PermissionState.None);
permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
//We want the sandboxer assembly's strong name, so that we can add it to the full trust list.
StrongName fullTrustAssembly = typeof(Sandboxer).Assembly.Evidence.GetHostEvidence<StrongName>();
//Now we have everything we need to create the AppDomain, so let's create it.
AppDomain newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, fullTrustAssembly);
//Use CreateInstanceFrom to load an instance of the Sandboxer class into the
//new AppDomain.
ObjectHandle handle = Activator.CreateInstanceFrom(
newDomain, typeof(Sandboxer).Assembly.ManifestModule.FullyQualifiedName,
typeof(Sandboxer).FullName
);
//Unwrap the new domain instance into a reference in this domain and use it to execute the
//untrusted code.
Sandboxer newDomainInstance = (Sandboxer) handle.Unwrap();
newDomainInstance.ExecuteUntrustedCode(untrustedAssembly, untrustedClass, entryPoint, parameters);
}
public void ExecuteUntrustedCode(string assemblyName, string typeName, string entryPoint, Object[] parameters)
{
//Load the MethodInfo for a method in the new Assembly. This might be a method you know, or
//you can use Assembly.EntryPoint to get to the main function in an executable.
MethodInfo target = Assembly.Load(assemblyName).GetType(typeName).GetMethod(entryPoint);
try
{
//Now invoke the method.
bool retVal = (bool)target.Invoke(null, parameters);
}
catch (Exception ex)
{
// When we print informations from a SecurityException extra information can be printed if we are
//calling it with a full-trust stack.
(new PermissionSet(PermissionState.Unrestricted)).Assert();
Console.WriteLine("SecurityException caught:\n{0}", ex.ToString());
CodeAccessPermission.RevertAssert();
Console.ReadLine();
}
}
}