shade - Buscando un enfoque práctico para los complementos de.NET de sandboxing
shade sandbox (5)
Estoy buscando una forma sencilla y segura de acceder a los complementos desde una aplicación .NET. Aunque imagino que este es un requisito muy común, me cuesta encontrar algo que satisfaga todas mis necesidades:
- La aplicación host descubrirá y cargará sus ensamblados de complementos en tiempo de ejecución
- Los complementos serán creados por terceros desconocidos, por lo que deben estar aislados para evitar que ejecuten código malicioso.
- Un ensamblaje de interoperabilidad común contendrá tipos a los que hace referencia tanto el host como sus complementos
- Cada conjunto de complemento contendrá una o más clases que implementan una interfaz de complemento común
- Al inicializar una instancia de complemento, el host pasará una referencia a sí mismo en forma de una interfaz de host
- El host llamará al plugin a través de su interfaz común y los plugins podrán llamar al host de la misma manera
- El host y los complementos intercambiarán datos en la forma de los tipos definidos en el ensamblaje de interoperabilidad (incluidos los tipos genéricos)
He investigado tanto al MEF como al MAF, pero estoy luchando para ver cómo se puede hacer que cualquiera de ellos se ajuste a la factura.
Suponiendo que mi entendimiento sea correcto, MAF no puede soportar el paso de tipos genéricos a través de su límite de aislamiento, lo cual es esencial para mi aplicación. (MAF también es muy complejo de implementar, pero estaría preparado para trabajar con esto si pudiera resolver el problema de tipo genérico).
MEF es casi una solución perfecta, pero parece no cumplir con los requisitos de seguridad, ya que carga sus ensamblajes de extensión en el mismo dominio de aplicación que el host y, por lo tanto, evita el sandboxing.
He visto esta pregunta , que habla de ejecutar MEF en un modo de espacio aislado, pero no describe cómo. Esta publicación indica que "cuando se utiliza MEF, debe confiar en las extensiones para que no ejecuten códigos maliciosos u ofrecer protección a través de la Seguridad de acceso al código", pero, nuevamente, no describe cómo. Finalmente, está esta publicación , que describe cómo evitar que se carguen complementos desconocidos, pero esto no es apropiado para mi situación, ya que incluso los complementos legítimos serán desconocidos.
He tenido éxito en la aplicación de atributos de seguridad .NET 4.0 a mis ensamblajes y MEF los respeta correctamente, pero no veo cómo esto me ayuda a bloquear el código malicioso, ya que muchos de los métodos del marco pueden ser una amenaza para la seguridad ( como los métodos de System.IO.File
) están marcados como SecuritySafeCritical
, lo que significa que son accesibles desde los ensamblajes de SecurityTransparent
. ¿Me estoy perdiendo de algo? ¿Hay algún paso adicional que pueda tomar para decirle a MEF que debería proporcionar privilegios de Internet a los ensamblajes de complementos?
Finalmente, también he analizado la creación de mi propia arquitectura de complementos de espacio aislado, utilizando un dominio de aplicación separado, como se describe here . Sin embargo, por lo que puedo ver, esta técnica solo me permite usar el enlace tardío para invocar métodos estáticos en clases en un ensamblaje no confiable. Cuando trato de ampliar este enfoque para crear una instancia de una de mis clases de complemento, la instancia devuelta no se puede convertir a la interfaz de complemento común, lo que significa que es imposible que la aplicación host la llame. ¿Existe alguna técnica que pueda usar para obtener un acceso de proxy fuertemente tipado a través del límite del dominio de aplicación?
Me disculpo por la duración de esta pregunta; el motivo fue mostrar todas las vías que ya he investigado, con la esperanza de que alguien pueda sugerir algo nuevo para probar.
Muchas gracias por tus ideas, Tim
Debido a que estás en diferentes AppDomains, no puedes simplemente pasar la instancia a través.
Deberá hacer que sus complementos sean remotos y crear un proxy en su aplicación principal. Echa un vistazo a los documentos para CreateInstanceAndUnWrap , que tiene un ejemplo de cómo todo esto podría funcionar hacia la parte inferior.
Esta también es otra descripción mucho más amplia por Jon Shemitz que creo que es una buena lectura. Buena suerte.
Gracias por compartir con nosotros la solución. Me gustaría hacer un comentario importante y una sugerencia.
El comentario es que no se puede hacer un 100% de un complemento de Sandbox al cargarlo en un dominio de aplicación diferente del host. Para averiguarlo, actualice DoSomethingDangerous a lo siguiente:
public override void DoSomethingDangerous()
{
new Thread(new ThreadStart(() => File.ReadAllText(@"C:/Test.txt"))).Start();
}
Una excepción no controlada provocada por un subproceso secundario puede bloquear toda la aplicación.
Lea this para obtener información sobre las excepciones de manejo.
También puede leer estas dos entradas de blog del equipo de System.AddIn que explican que el 100% de aislamiento solo puede tener lugar cuando el complemento se encuentra en un proceso diferente. También tienen un ejemplo de lo que alguien puede hacer para recibir notificaciones de complementos que no pueden manejar las excepciones planteadas.
Ahora la sugerencia que quería hacer tiene que ver con el método PluginFinder.FindPlugins. En lugar de cargar cada conjunto candidato en un nuevo dominio de aplicación, reflexionando sobre sus tipos y la descarga del dominio de aplicación, podría usar Mono.Cecil . Entonces no tendrás que hacer nada de esto.
Es tan simple como:
AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(assemblyPath);
foreach (TypeDefinition td in ad.MainModule.GetTypes())
{
if (td.BaseType != null && td.BaseType.FullName == "MyNamespace.MyTypeName")
{
return true;
}
}
Probablemente haya mejores maneras de hacer esto con Cecil, pero no soy un usuario experto de esta biblioteca.
Saludos,
He aceptado la respuesta de Alastair Maw, ya que fue su sugerencia y enlaces los que me llevaron a una solución viable, pero estoy publicando aquí algunos detalles de lo que hice exactamente, para cualquier otra persona que intente lograr algo similar.
Como recordatorio, en su forma más simple, mi aplicación comprende tres ensamblajes:
- El principal ensamblaje de la aplicación que consumirá plugins.
- Un ensamblaje de interoperabilidad que define tipos comunes compartidos por la aplicación y sus complementos
- Un ensamble de plugin de muestra
El siguiente código es una versión simplificada de mi código real, que muestra solo lo que se necesita para descubrir y cargar complementos, cada uno en su propio AppDomain
:
A partir del ensamblaje principal de la aplicación, la clase del programa principal utiliza una clase de utilidad llamada PluginFinder
para descubrir los tipos de complementos que califican dentro de cualquier ensamblaje en una carpeta de complementos designada. Para cada uno de estos tipos, luego crea una instancia de un dominio de aplicación sandox (con permisos de zona de Internet) y la usa para crear una instancia del tipo de complemento descubierto.
Al crear un AppDomain
con permisos limitados, es posible especificar uno o más conjuntos confiables que no están sujetos a esos permisos. Para lograr esto en el escenario presentado aquí, el ensamblaje de la aplicación principal y sus dependencias (el ensamblaje de interoperabilidad) deben estar firmados.
Para cada instancia de complemento cargado, los métodos personalizados dentro del complemento pueden llamarse a través de su interfaz conocida y el complemento también puede volver a llamar a la aplicación host a través de su interfaz conocida. Finalmente, la aplicación host descarga cada uno de los dominios de sandbox.
class Program
{
static void Main()
{
var domains = new List<AppDomain>();
var plugins = new List<PluginBase>();
var types = PluginFinder.FindPlugins();
var host = new Host();
foreach (var type in types)
{
var domain = CreateSandboxDomain("Sandbox Domain", PluginFinder.PluginPath, SecurityZone.Internet);
plugins.Add((PluginBase)domain.CreateInstanceAndUnwrap(type.AssemblyName, type.TypeName));
domains.Add(domain);
}
foreach (var plugin in plugins)
{
plugin.Initialize(host);
plugin.SaySomething();
plugin.CallBackToHost();
// To prove that the sandbox security is working we can call a plugin method that does something
// dangerous, which throws an exception because the plugin assembly has insufficient permissions.
//plugin.DoSomethingDangerous();
}
foreach (var domain in domains)
{
AppDomain.Unload(domain);
}
Console.ReadLine();
}
/// <summary>
/// Returns a new <see cref="AppDomain"/> according to the specified criteria.
/// </summary>
/// <param name="name">The name to be assigned to the new instance.</param>
/// <param name="path">The root folder path in which assemblies will be resolved.</param>
/// <param name="zone">A <see cref="SecurityZone"/> that determines the permission set to be assigned to this instance.</param>
/// <returns></returns>
public static AppDomain CreateSandboxDomain(
string name,
string path,
SecurityZone zone)
{
var setup = new AppDomainSetup { ApplicationBase = Path.GetFullPath(path) };
var evidence = new Evidence();
evidence.AddHostEvidence(new Zone(zone));
var permissions = SecurityManager.GetStandardSandbox(evidence);
var strongName = typeof(Program).Assembly.Evidence.GetHostEvidence<StrongName>();
return AppDomain.CreateDomain(name, null, setup, permissions, strongName);
}
}
En este código de ejemplo, la clase de aplicación de host es muy simple, exponiendo solo un método al que pueden llamar los complementos. Sin embargo, esta clase debe derivarse de MarshalByRefObject
para que pueda ser referenciada entre dominios de aplicación.
/// <summary>
/// The host class that exposes functionality that plugins may call.
/// </summary>
public class Host : MarshalByRefObject, IHost
{
public void SaySomething()
{
Console.WriteLine("This is the host executing a method invoked by a plugin");
}
}
La clase PluginFinder
solo tiene un método público que devuelve una lista de los tipos de complementos descubiertos. Este proceso de descubrimiento carga cada ensamblaje que encuentra y utiliza la reflexión para identificar sus tipos calificados. Como este proceso puede potencialmente cargar muchos ensamblajes (algunos de los cuales ni siquiera contienen tipos de complementos), también se ejecuta en un dominio de aplicación separado, que puede ser descargado posteriormente. Tenga en cuenta que esta clase también hereda MarshalByRefObject
por los motivos descritos anteriormente. Como las instancias de Type
no se pueden pasar entre los dominios de la aplicación, este proceso de descubrimiento usa un tipo personalizado llamado TypeLocator
para almacenar el nombre de la cadena y el nombre del ensamblaje de cada tipo descubierto, que luego puede pasarse de nuevo al dominio principal de la aplicación.
/// <summary>
/// Safely identifies assemblies within a designated plugin directory that contain qualifying plugin types.
/// </summary>
internal class PluginFinder : MarshalByRefObject
{
internal const string PluginPath = @"../../../Plugins/Output";
private readonly Type _pluginBaseType;
/// <summary>
/// Initializes a new instance of the <see cref="PluginFinder"/> class.
/// </summary>
public PluginFinder()
{
// For some reason, compile-time types are not reference equal to the corresponding types referenced
// in each plugin assembly, so equality must be tested by loading types by name from the Interop assembly.
var interopAssemblyFile = Path.GetFullPath(Path.Combine(PluginPath, typeof(PluginBase).Assembly.GetName().Name) + ".dll");
var interopAssembly = Assembly.LoadFrom(interopAssemblyFile);
_pluginBaseType = interopAssembly.GetType(typeof(PluginBase).FullName);
}
/// <summary>
/// Returns the name and assembly name of qualifying plugin classes found in assemblies within the designated plugin directory.
/// </summary>
/// <returns>An <see cref="IEnumerable{TypeLocator}"/> that represents the qualifying plugin types.</returns>
public static IEnumerable<TypeLocator> FindPlugins()
{
AppDomain domain = null;
try
{
domain = AppDomain.CreateDomain("Discovery Domain");
var finder = (PluginFinder)domain.CreateInstanceAndUnwrap(typeof(PluginFinder).Assembly.FullName, typeof(PluginFinder).FullName);
return finder.Find();
}
finally
{
if (domain != null)
{
AppDomain.Unload(domain);
}
}
}
/// <summary>
/// Surveys the configured plugin path and returns the the set of types that qualify as plugin classes.
/// </summary>
/// <remarks>
/// Since this method loads assemblies, it must be called from within a dedicated application domain that is subsequently unloaded.
/// </remarks>
private IEnumerable<TypeLocator> Find()
{
var result = new List<TypeLocator>();
foreach (var file in Directory.GetFiles(Path.GetFullPath(PluginPath), "*.dll"))
{
try
{
var assembly = Assembly.LoadFrom(file);
foreach (var type in assembly.GetExportedTypes())
{
if (!type.Equals(_pluginBaseType) &&
_pluginBaseType.IsAssignableFrom(type))
{
result.Add(new TypeLocator(assembly.FullName, type.FullName));
}
}
}
catch (Exception e)
{
// Ignore DLLs that are not .NET assemblies.
}
}
return result;
}
}
/// <summary>
/// Encapsulates the assembly name and type name for a <see cref="Type"/> in a serializable format.
/// </summary>
[Serializable]
internal class TypeLocator
{
/// <summary>
/// Initializes a new instance of the <see cref="TypeLocator"/> class.
/// </summary>
/// <param name="assemblyName">The name of the assembly containing the target type.</param>
/// <param name="typeName">The name of the target type.</param>
public TypeLocator(
string assemblyName,
string typeName)
{
if (string.IsNullOrEmpty(assemblyName)) throw new ArgumentNullException("assemblyName");
if (string.IsNullOrEmpty(typeName)) throw new ArgumentNullException("typeName");
AssemblyName = assemblyName;
TypeName = typeName;
}
/// <summary>
/// Gets the name of the assembly containing the target type.
/// </summary>
public string AssemblyName { get; private set; }
/// <summary>
/// Gets the name of the target type.
/// </summary>
public string TypeName { get; private set; }
}
El ensamblaje de interoperabilidad contiene la clase base para las clases que implementarán la funcionalidad del complemento (tenga en cuenta que también se deriva de MarshalByRefObject
.
Este conjunto también define la interfaz IHost
que permite que los complementos vuelvan a llamar a la aplicación host.
/// <summary>
/// Defines the interface common to all untrusted plugins.
/// </summary>
public abstract class PluginBase : MarshalByRefObject
{
public abstract void Initialize(IHost host);
public abstract void SaySomething();
public abstract void DoSomethingDangerous();
public abstract void CallBackToHost();
}
/// <summary>
/// Defines the interface through which untrusted plugins automate the host.
/// </summary>
public interface IHost
{
void SaySomething();
}
Finalmente, cada complemento deriva de la clase base definida en el ensamblaje de interoperabilidad e implementa sus métodos abstractos. Puede haber varias clases heredadas en cualquier conjunto de complementos y puede haber múltiples conjuntos de complementos.
public class Plugin : PluginBase
{
private IHost _host;
public override void Initialize(
IHost host)
{
_host = host;
}
public override void SaySomething()
{
Console.WriteLine("This is a message issued by type: {0}", GetType().FullName);
}
public override void DoSomethingDangerous()
{
var x = File.ReadAllText(@"C:/Test.txt");
}
public override void CallBackToHost()
{
_host.SaySomething();
}
}
Si necesita que sus extensiones de terceros se carguen con privilegios de seguridad más bajos que el resto de su aplicación, debe crear un nuevo dominio de aplicación, crear un contenedor MEF para sus extensiones en ese dominio de la aplicación y luego hacer llamadas desde su aplicación a los objetos. en el dominio de la aplicación sandbox. El sandboxing se produce en la forma en que creas el dominio de la aplicación, no tiene nada que ver con MEF.
Una alternativa sería usar esta biblioteca: https://processdomain.codeplex.com/ Le permite ejecutar cualquier código .NET en AppDomain fuera de proceso, lo que proporciona un aislamiento aún mejor, que la respuesta aceptada. Por supuesto, uno tiene que elegir una herramienta adecuada para su tarea y, en muchos casos, todo lo que se necesita es el enfoque dado en la respuesta aceptada.
Sin embargo, si está trabajando con complementos .net que invocan bibliotecas nativas que pueden ser inestables (la situación que encontré personalmente) desea ejecutarlos no solo en un dominio de aplicación separado, sino también en un proceso separado. Una buena característica de esta biblioteca es que reiniciará automáticamente el proceso si un complemento lo bloquea.