Fabio Galuppo |
Se encuentran en .NET Framework SDK\Reference\Design Guidelines for Class Library Developers. Las mismas traen información y buenas prácticas sobre: nomenclaturas, patrones de manejo de errores, patrones de diseño, threading, y programación asincrónica, entre otros temas.
Uno de lo ítems que me llamó la atención es sobre seguridad, peor no sobre características de seguridad, sino sobre cómo escribir código seguro. Los autores Michael Howard y David LeBlanc definen en su libro Writing Secure Code, Microsoft Press, que el código seguro es también código robusto.
A pesar de todos estas guidelines y best-practices, algunas cosas se nos pasan desapercibidas, y por falta de tiempo acabamos dejando uno u otro punto de lado.
Para resolver esto, el equipo de GotDotNet, creo el utilitario FxCop que analiza el assembly verificando si las buenas prácticas descriptas en las guidelines dueron aplicadas. Mi sugerencia es que entre al sitio de GotDotNet y descargue esta herramienta, que lo ayuda en la creación de un código más robusto.
El punto principal aquí es que siguiendo estas prácticas, no tendrá garantía de producir código 100% seguro, pero será un buen cominezo, y con certeza estará aumentando el nivel de seguridad de su assembly.
FXCop
Esta herramientas definida como un analizador de buenas brácticas de programación para la plataforma .NET posee una versión GUI (figura 1) y otra de línea de comando (fxcopcmd.exe).
A través de reflection esta herramienta analiza y propone algunas mejoras y fixes al código, para tornarlo más robusto y seguro. Estas sugerencias van en orden de prioridad en una lista de violaciones.
Mirando el programa de abajo, percibimos que es funcionalmente correcto.
using System;
using System.IO;
using System.Security.Principal;
public delegate void Message();
public class MyApp
{
public static event Message PreProcessing;
public static event Message PosProcessing;
public static void Main()
{
PreProcessing += new Message(OnPreProcessing);
PosProcessing += new Message(OnPosProcessing);
FileStream fs = new FileStream("info.txt", FileMode.Open, FileAccess.Read);
int length = (int) new FileInfo("info.txt").Length;
byte[] file = new byte[length];
fs.Read(file, 0, length);
fs.Close();
WindowsPrincipal wp = new WindowsPrincipal(WindowsIdentity.GetCurrent());
Console.WriteLine("\nRunAs : {0}", wp.Identity.Name);
PreProcessing();
for( int i = 0; i < length; ++i ) Console.Write( (char) file[i] );
PosProcessing();
}
public static void OnPreProcessing()
{
Console.WriteLine("\n********* Iniciando a leitura do arquivo info.txt *********");
}
public static void OnPosProcessing()
{
Console.WriteLine("\n********* Finalizando a leitura do arquivo info.txt *********");
}
}
Sin embargo, considerar esto como funcional, sería carecer de visión crítica, o ser un programador inexperto. Verificando atentamente con FxCop, obtenemos el siguiente diagnóstico (figura 1).
Figura 1: FxCop en acción
Para una pequeña aplicación que lee un archivo de texto la lista de violaciones es extensa. Las apuntadas son referentes a seguridad, tipos, evidencia, COM, eventos, identificación, parámetros y namespace. Las reglas de verificación son determinadas a través del tab Rules externo y son divididas en: COMRules, DesignRules, GlobalizationRules, NamingRules, PerformanceRules, SecurityRules y UsageRules. Estas reglas pueden ser filtradas.
Cuando FxCop ejecuta una análisis (Analyze Checked Assemblies), se exhiben las violaciones y las reglas en que éstas encajan. Estas reglas se encuentran en el tab Rules interno. En la figura 2, tenemos la lista de las violaciones del assembly producido. Fíjese que se muestra la prioridad o "gravedad" de la violación, donde el menor valor es el más grave; la violación y el lugar (tipo o miembro) ocurrida.
Figura 2: Detalles de las violaciones en el assembly file.exe
Antes de adentrarnos en las violaciones, vamos a analizar la ejecucución del assembly. Si el mismo corre con autenticación de Administrator, el comportamiento presentado en la figura 3 está correcto:
Figura 3: Aplicación corriendo con autenticación de Administrator
Sin embargo si intenta correr el assembly, por ejemplo, como Guest, esto no sería correcto.
Vamos a simularlo a través de la opción de ejecución RunAs, que utiliza internamente la API Win32 CreateProcessAsUser, para correr la aplicación en otra cuenta.
El resulltado indeseado se ve en la figura 4, o sea, una cuenta que no estaba en el grupo de administradores no debería tener acceso al archivo.
Como vemos, tenemos una falla de seguridad en esta aplicación y este tipo de falla pasa desapercibido para quien mira la aplicación en forma funcional. Esto abre brechas para los "curiosos".
Por lo tanto, escribir código robusto y seguro es obligación de cada desarrollador que piensa en la calidad.
Figura 4: Aplicación corriendo con autenticación de Guest
Asertando el código y protegiendo los recursos
Para asertar el código debemos seguir las sugerncias de FxCop, así como otras no sugeridas explícitamente.
Una de las violaciones apuntadas indica la falta de un strong-name. Este strong-name se obtiene a través del utilitario sn.exe del Framework .NET. Utilice la línea de comando para obtener el par de claves (pública/privada):
sn -k keyfile.snk
Figura 5: Detalles de violación (Properties)
El par de claves será insertado en la metadata del assembly a través del atributo AssemblyKeyFile, encontrado en el namespace System.Reflection.
[assembly: AssemblyKeyFile("keyfile.snk")]
Otro atributo que deberá ser embebido es la versión del assembly, AssemblyVersion.
[assembly: AssemblyVersion("1.0.1.0")]
Un strong-name refuesrza la identidad del assembly evitando "falsificaciones". Solamente un assembly con strong-name puede ser compartido y depositado en la Global Assembly Cache (GAC).
Recompilando la aplicación con estos cambios y analizando con FxCop el assembly producido, notará que las violaciones - Assemblies have strong names y Assemblies have version numbers - se fueron.
La interfaz de la herramienta FxCop es intuitiva y simple. Agrega assemblies, determina las reglas y verifica las violaciones.
Otra funcionalidad interesante está relacionada con la verificación de una violación. Si selecciona son el botón derecho una violación, un pop-up surgirá con la opción Properties disponible. Al seleccionarla aparecerá una ventana con información y sugerencias de como resolverla. En la figura 5 vemos la sugerencia para hacer al assembly CLS-compliant, esto quiere decir que los miembros de visulazación externa deberán contener sólo tipos CLS-compliant, o sea que tipos como Uint32 (uint en C#) estarán prohibidos.
Para resolver el problema de la figura 5, agregue el atributo CLSCompliant:
[assembly: CLSCompliant(true)]
Siguiendo con el análisis, notamos que una de las violaciones surgidas es sobre namespace y tipos. Por lo tanto, declare todos los tipos dentro de un namspace para que no haya colisión de estos con otros escritos por terceros:
namespace Desenvolvendo.Net
{
public delegate void Message();
public class MyApp
{
... //implentação
}
}
Ahora nuestra lista aumentó, porque tras la colocación del namespace Desenvolvendo.Net (en este formato, o sea usando Pascal Case), la herramienta nos indica una violaciónde priporidad 3 - Avoid having a namespace with a small number of types. Bueno, por tratarse de un ejemplo podemos ignorar esta violación y excluirla de la lista, haciendo clic derecho y seleccionando el ítem Exclude. ASí excluirá esta violación. Puede hacer lo mismo con otras, pero ¡hágalo en forma consciente!
Otra violación se refiere a los eventos ya que en vez de usar el delegado Message se sugiere utilizar el delegato EventHandler o uno similar, que traiga los parámetros de un objeto como originador del evento (sender) y los argumentos del evento. De acuerdo a esto, vamos a actualiar el delegado Message y los eventos PreProcessing y PosProcessing. Para seguir las reglas de nomenclatura, el sufijo EventHandler deberá ser agregado al delegado Message. Abajo, los cambios efectuados:
public delegate void MessageEventHandler(object sender, EventArgs e);
public static event MessageEventHandler PreProcessing;
public static event MessageEventHandler PosProcessing;
PreProcessing += new MessageEventHandler(OnPreProcessing);
PosProcessing += new MessageEventHandler(OnPosProcessing);
public static void OnPreProcessing(object sender, EventArgs e)
{
... //implentação
}
public static void OnPosProcessing(object sender, EventArgs e)
{
... //implentação
}
Para que el assembly sea visible a un cliente COM, como por ejemplo una aplicación Visual Basic 6, es necesario exportar los miembros de instancia. En el ejemplo anterior, los miembros principales de la aplicación son enstáticos. Debemos hacer una reingeniería para tornarlos miembros de instancia. Mi sugerencia es crear una nueva clase que sea instanciada dentro de la aplicación.
Así fue creada la claseMyObjectClass que elevó las immplementaciones de los miembros de la clase MyApp, que simplemente instancias un objeto de la clase MyObjectClass y ejecuta el método Process para realizar la tarea.
public class MyObjectClass
{
public event MessageEventHandler PreProcessing;
public event MessageEventHandler PosProcessing;
public void Process()
{ ... //implentação }
public void OnPreProcessing(object sender, EventArgs e)
{ ... //implentação }
public void OnPosProcessing(object sender, EventArgs e)
{ ... //implentação }
}
public class MyApp
{
public static void Main()
{
MyObjectClass o = new MyObjectClass();
o.Process();
}
}
En el caso de la última violación, es referente al consumo de un cliente COM, ya que un cliente COM no puede consumir un assembly ejecutable (exe). Lo que debemos hacer es aislar la funcionalidad en una biblioteca para que esto sea posible. Así que aislaremos la clase MyObjectClass para que pueda ser consumida por un cliente COM.
Como requisito de las reglas de COM determinadas en las guideline, que no ignoraremos, debemos colocar un atributo ComVisible indicando si una clase tendrá (true) o no (false) acesso a través de COM. El atributo ComVisible se encuantra en el namespace System.Runtime.InteropServices. Adecuando el assembly para acesso a través de COM, tenemos:
[assembly: CLSCompliant(true)]
[ComVisible(false)]
public delegate void MessageEventHandler(object sender, EventArgs e);
[ComVisible(false)]
public class MyObjectClass
{
public event MessageEventHandler PreProcessing;
public event MessageEventHandler PosProcessing;
[ComVisible(false)]
public void Process()
{ ... //implentação }
... //outros membros
}
[ComVisible(false)]
public class MyApp
{
public static void Main()
{ ... //implentação }
}
La próxima violación a ser eliminada es tornar la clase que procesa la aplicación inaccesible externamente.
class MyApp
{
static void Main()
{ ... //implentação }
}
Según la herramienta FxCop, el último tema pendiente son los permisos del assembly. En este caso, debemos determinar los permisos que son posibles. Uno puede ser el permiso de lectura del archivo de texto (info.txt). Este permiso lo daremos usando la forma declarativa, a través del atributo FileIOPermission del namespace System.Security.Permissions:
[assembly: FileIOPermission(SecurityAction.RequestMinimum,
Read = @"C:\Articles\Desenvolvendo.NET\article 1\sample 2\info.txt")]
¡Con esto eliminamos todas las violaciones apuntadas por FxCop en este assembly!
Algunos trucos que no son indicados en el análisis de FxCop:
- Separar las clases ejecutoras de las clases funcionales. En este caso separar la clase MyApp que instancia y ejecuta la clase MyObjectClass. La clase MyObjectClass debe estar depositada en un assembly separado, preferentemente una biblioteca (dll) para que pueda ser reutilizada por otros clientes, por ejemplo un cliente COM;
- Si la clase MyObjectClass no se planeó para ser heredada, utilice el especificados de clase sealed;
- Miembros que no deban ser consumidos externamente debe se especificados como private o protected.
Cambiando el assembly con estas best-practices tenemos:
//csc /t:library FileLib.cs
using System;
using System.IO;
using System.Security.Principal;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Permissions;
[assembly: AssemblyKeyFile("keyfile.snk")]
[assembly: AssemblyVersion("1.0.2.0")]
[assembly: CLSCompliant(true)]
[assembly: ComVisible(true)]
[assembly: FileIOPermission(SecurityAction.RequestMinimum, Read =
@"C:\Articles\Desenvolvendo.NET\article 1\sample 2\info.txt")]
namespace Desenvolvendo.Net
{
[ComVisible(false)]
public delegate void MessageEventHandler(object sender, EventArgs e);
[ComVisible(false)]
public sealed class MyObjectClass
{
public event MessageEventHandler PreProcessing;
public event MessageEventHandler PosProcessing;
[ComVisible(true)]
public void Process()
{
PreProcessing += new MessageEventHandler(OnPreProcessing);
PosProcessing += new MessageEventHandler(OnPosProcessing);
string filename = @"C:\Articles\Desenvolvendo.NET\article 1\sample 2\info.txt";
FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read);
int length = (int) new FileInfo(filename).Length;
byte[] file = new byte[length];
fs.Read(file, 0, length);
fs.Close();
WindowsPrincipal wp = new WindowsPrincipal(WindowsIdentity.GetCurrent());
Console.WriteLine("\nRunAs : {0}", wp.Identity.Name);
PreProcessing(this, null);
for( int i = 0; i < length; ++i ) Console.Write( (char) file[i] );
PosProcessing(this, null);
}
private void OnPreProcessing(object sender, EventArgs e)
{
Console.WriteLine("\n********* Iniciando a leitura do arquivo info.txt *********");
}
private void OnPosProcessing(object sender, EventArgs e)
{
Console.WriteLine("\n********* Finalizando a leitura do arquivo info.txt *********");
}
}
}
//csc FileApp.cs /r:FileLib.dll
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using Desenvolvendo.Net;
[assembly: AssemblyKeyFile("keyfile.snk")]
[assembly: AssemblyVersion("1.0.2.0")]
[assembly: CLSCompliant(true)]
[assembly: ComVisible(false)]
namespace Desenvolvendo.Net
{
class MyApp
{
static void Main()
{ MyObjectClass o = new MyObjectClass();
o.Process();
}
}
}
Finalizaremos con el último pediente, permitiendo que sólo los usuarios del grupo Administradores ejecute y acceda al archivo (info.txt). La aplicación podrá ser ejecutada sólo en la máquina local. Haremos esto de forma directa e imperativa. Ignoraremos de hecho la configuración de las ACLs del archivo.
En el archivo FileApp.cs, se usará la forma imperativa para determinar que la aplicación corra en la máquina local, con el permiso ZoneIdentityPermission del namespace System.Security.Permissions. Note que la enumeración SecurityZone.MyComputer es pasada al construtor, y se encuentra en System.Security. La línea de abajo será agregada al método Main antes de cualquier instrucción:
new ZoneIdentityPermission(SecurityZone.MyComputer).Demand();
En el archivo FileLib.cs, será usada la forma directa para verificar el usuario. Cambie el siguiente fragmento del método Process:
WindowsPrincipal wp = new WindowsPrincipal(WindowsIdentity.GetCurrent());
Console.WriteLine("\nRunAs : {0}", wp.Identity.Name);
PreProcessing(this, null);
for( int i = 0; i < length; ++i ) Console.Write( (char) file[i] );
PosProcessing(this, null);
Por:
WindowsIdentity wid = WindowsIdentity.GetCurrent();
Console.WriteLine("\nRunAs : {0}", wid.Name);
WindowsPrincipal wip = new WindowsPrincipal(wid);
if (wip.IsInRole(WindowsBuiltInRole.Administrator))
{
PreProcessing(this, null);
for( int i = 0; i < length; ++i ) Console.Write( (char) file[i] );
PosProcessing(this, null);
}
else
{
Console.WriteLine("Sem permissão");
}
La aplicación correrá como vimos en la figura 3 y será limitada de esta forma (figura 6).
Figura 6: Aplicación no ejecutada si no es por el grupo Administrators
Finalmente grabe el proyecto de análisi con FxCop. Un archivo con extensión .FxCop será generado. En el futuro éste podrá ser usado para tests de regresión o nuevos tests.
Como he dicho anteriormente la herramienta FxCop posee una interfaz simple e intuitiva. En la figura 7 tenemos parte de ella. En la barra de herramientas de izquierda a derecha tenemos: Nuevo proyecto, Abrir proyecto, Guardar proyecto, Guardar log de violaciones, Agregar assembly, Quitar assemblies selecionados, Analizar assemblies seleccionados y Enviar por mail un reporte de bug.
Figura 7: FxCop
Conclusión
Escribir código seguro es obligación de cada desarrollador. Simplemente considerar el código en forma funcional es un error.
La herramienta FxCop nos ayuda a aplicar las mejores prácticas de programación en la plataforma .NET. Entienda estas reglas y sus objetivos. Además de esta herramienta, algunos otros cuidados se han presentado que harán a su código más robusto.
Siempre esté atento a cada detalle de su aplicación y de sus requisitos. Adquiera el hábito de ver sus programas con "otros ojos", revise su código varias veces y tenga en mente cuáles son las características que son absolutamente prioritarias.
Source Code
Fabio Galuppo es Microsoft Certified Solution Developer (MCSD), y Microsoft Certified Professional (MCP). Arquitecto y Desarrollador. Columnista para el sitio web de MSDN Brazil (Unleashing the .NET Framework) y otros. Co-fundador del sitio web Developing.NET, dedicado exclusivamente a profesionales .NET. Trabajó en varios proyectos incluyendo .NET, Windows DNA, SAP R/3 e integración de sistemas. Sus áreas de interes, desarrollo e investigación incluyen: el Framework .NET, Rotor, COM(+), Win32/Win64, NT Kernel, XML y C/C++.