DNM+ Online
dotnetmania 2.0
Más sobre seguridad de acceso a código
En una entrega anterior (dotNetManía nº 41) presentamos los fundamentos de la Seguridad de acceso a código (Code Access Security, CAS) en .NET Framework, y describimos cómo el CLR determina los permisos del código y cómo se pueden manipular desde las herramientas administrativas los permisos concedidos a cada ensamblado. En esta entrega veremos cómo se pueden solicitar, manipular y limitar estos permisos desde dentro de nuestro código.

Atributos para solicitud
y rechazo de permisos Un primer paso que podemos dar en nuestros programas es el de "decorar" el código fuente con una serie de atributos en los que se especifican los requisitos que tiene el ensamblado en cuanto a los permisos que necesita para poder funcionar. Cuando especificamos los requisitos de seguridad mediante atributos, se dice que estamos haciendo operaciones de seguridad declarativas, en contraposición a las que se realizan mediante sentencias de código ejecutable, que se denominan imperativas. En su modalidad más simple, la solicitud de permisos puede tener este aspecto: Estos atributos, que afectan a todo el ensamblado, se pueden introducir en cualquiera de los fuentes que se compilan para formar dicho ensamblado; pero es costumbre, para facilitar su localización, agruparlos dentro del archivo AssemblyInfo.cs (o .vb). En el ejemplo anterior hemos utilizado un permiso del tipo UIPermission, es decir, una solicitud de permiso de interfaz de usuario. En los argumentos hemos especificado SecurityAction.RequestMinimum, indicando que el programa necesita como mínimo este permiso para poder funcionar. Si las políticas de seguridad del sistema no concediesen a este ensamblado los permisos que hemos señalado como "mínimos", el ensamblado no llegaría a cargarse, y el sistema mostraría directamente un error de seguridad, sin siquiera arrancar el programa. El parámetro Window=UIPermissionWindow.AllWindows especifica en mayor detalle el tipo concreto de UIPermission que estamos solicitando. Para cada subtipo de Permission, los argumentos disponibles son diferentes y permiten matizar las características y alcance del permiso correspondiente. Cabe señalar que también es posible solicitar de golpe un conjunto de permisos completo. Por ejemplo, para solicitar el conjunto "Full Trust": De forma similar a la solicitud de permisos mínimos, se pueden añadir solicitudes de permisos Opcionales y Rechazados: Con el primero de estos atributos estamos indicando al sistema de seguridad que deseamos utilizar el permiso de FileIO si las políticas de seguridad lo permiten para este ensamblado, pero que el programa puede funcionar sin este permiso. Esto puede ser útil, por ejemplo, para un programa que grabe una bitácora (log) de sus operaciones en un archivo en disco, pero que en caso de no tener ese permiso puede seguir funcionando; eso sí, sin grabar el log. El segundo atributo expresa que no deseamos disponer de permiso de acceso al Registro de Windows, incluso aunque las políticas de seguridad lo consientan para nuestro ensamblado. De esta forma garantizamos que el programa nunca pueda modificar el Registro, incluso aunque presente un mal funcionamiento o un uso indebido de alguna función de librería que pudiera realizar ese tipo de operación. La asignación final de permisos que recibe el ensamblado consiste en los Mínimos, más los Opcionales que permita la política de seguridad, menos los Rechazados. Hay que señalar que, en caso de que no se solicite ningún permiso opcional, el sistema interpreta que se solicita "Full Trust" como permiso opcional, y por tanto se conceden todos los permisos que permita la política, salvo los rechazados expresamente. Por este motivo, es buena costumbre incluir un atributo como el siguiente: Con esto indicamos que solicitamos como Opcional "ningún permiso", con lo que ya no se aplica lo indicado en el párrafo anterior, y los únicos permisos concedidos son los que solicitemos expresamente mediante el resto de los atributos. De esta forma, el programa no recibe ningún permiso superfluo. Solicitud de permisos ("Demand") Hemos visto que un ensamblado puede solicitar y recibir una serie de permisos. Los permisos que ha recibido el ensamblado entrarán en juego en el momento en que el código que se ejecuta realice una operación que requiera dichos permisos. ¿Cómo se determinan y se controlan estas operaciones? Veámoslo, apoyándonos en la figura 1. Supongamos que tenemos un programa ejecutable, que en la figura denominamos "Ensamblado 1", que conforme con las políticas del sistema ha recibido permiso de ejecución pero no de escritura en disco. Este programa realiza una llamada a una DLL ("Ensamblado 2") que graba ciertos datos en disco. Este ensamblado, al igual que el anterior, habrá recibido los permisos que le correspondan; por ejemplo, podría tener permiso de escritura en disco. Para realizar físicamente la grabación, esta DLL utiliza las rutinas contenidas en el espacio de nombres System.IO, que se encuentran compiladas dentro de una de las DLL que se suministran con .NET Framework, y que en la figura hemos llamado "Ensamblado 3". Esta librería, en última instancia, tendrá que utilizar los servicios de invocación a la plataforma (P/Invoke) para llamar a las API de Windows que realizan la grabación mediante código no manejado. Antes de hacer el salto a dicho código no manejado, el programa hace una llamada al código de seguridad de .NET que se denomina "solicitud de permiso" ("Demand"). En el listado 1 tenemos un ejemplo que muestra una solicitud de este tipo. Cuando se ejecuta el Demand(), el motor de seguridad realiza un recorrido por la pila de llamadas (stack walk), comprobando si todos los llamantes de esta rutina tienen el permiso solicitado. En el ejemplo de la figura 1, la solicitud sube en primer lugar al Ensamblado 2, que efectivamente tenía permiso de escritura, con lo que la solicitud continúa subiendo por la pila. Al llegar al Ensamblado 1, el CLR determina que dicho ensamblado no tiene permiso de escritura, por lo que se produce una excepción de seguridad y la escritura no llega a realizarse. El resultado final es que no se produce ninguna escritura en disco, a pesar de que las DLL que contienen instrucciones de grabación tienen permiso para ello, debido a que el llamante inicial tiene prohibida dicha escritura por las políticas de seguridad de .NET. Estas  solicitudes también se pueden hacer de forma declarativa en lugar de imperativa, a nivel de clase o de método: Afirmación de permisos ("Assert") Vayamos más allá y supongamos que realmente sí que deseamos realizar la grabación anterior. Por ejemplo, el Ensamblado 2 podría contener unas rutinas de registro en bitácora (logging) que graben en un archivo una traza de las operaciones realizadas, y deseamos que esa traza quede grabada siempre que un ejecutable llame a esta librería, aunque el ejecutable en sí no tenga permiso para escribir en disco. Esto se logra mediante la operación llamada "Assert", tal como muestra el ejemplo del listado 2. Volviendo al ejemplo de la figura 1, y suponiendo que el Assert() se ha introducido en el Ensamblado 2, cuando se produce el Demand() en el Ensamblado 3 y "sube" a la trama anterior de la pila, se encuentra que en dicha trama se ha realizado una afirmación del permiso solicitado, y en consecuencia se da dicho permiso por concedido, teniendo éxito el Demand() sin llegar a subir hasta el Ensamblado 1. Lógicamente, este proceso solo tendrá éxito si la política de seguridad concede el permiso solicitado al ensamblado que realiza el Assert. Solo puede haber un único Assert activo por cada trama de la pila. Una vez que el Assert ya no se necesita, se desactiva mediante RevertAssert(), como se ve en el ejemplo del listado 2. Hay que actuar con precaución al escribir ensamblados que utilicen el Assert, ya que por esta vía se pueden abrir agujeros de seguridad. Por ejemplo, en el listado 2 hemos especificado como const string el nombre del fichero en el que se graba el log. Si en lugar de eso hubiéramos aceptado este fichero como parámetro del método, el programa llamante (que, recordemos, no tiene permisos para grabar en disco) podría pasar por esta vía una ruta cualquiera y "engañar" al sistema de seguridad consiguiendo grabar en disco un archivo de su elección, aprovechándose de los permisos que tiene concedidos el Ensamblado 2.

Rechazo de permisos ("Deny") La operación "Deny" funciona del modo contrario que Assert. Cuando ejecutamos un Deny(), estamos forzando que se produzca una excepción cuando el código más abajo en la pila realice un Demand() del permiso objeto del Deny, incluso aunque el ensamblado llamante sí que tenga el permiso en cuestión. El ejemplo del listado 3 rechaza el permiso de utilizar reflexión sobre miembros no visibles de un tipo. Si este código se introdujera en el Ensamblado 2 de la figura 1, y detrás del Deny se llamara a una subrutina del Ensamblado 3 que tratara de usar reflexión sobre miembros privados de una clase, se produciría una excepción de seguridad, aún en el caso de que tanto el Ensamblado 3 como el 2 tuvieran concedido dicho permiso.

"LinkDemand" La operación "LinkDemand" opera de forma similar al Demand que hemos mencionado más arriba, con la diferencia de que LinkDemand() solo comprueba el llamante inmediato, en lugar de recorrer toda la pila comprobando si la totalidad de los llamantes tienen el permiso solicitado. Por ejemplo, para comprobar que el llamante tiene "Full Trust": Adicionalmente, el Demand recorre la pila cada vez que es invocado, debido a que los llamantes podrían ser distintos, mientras que el LinkDemand se resuelve en tiempo de compilación Just-In-Time (JIT), por lo que solo se comprueba una vez.

El atributo AllowPartiallyTrustedCallers Todo ensamblado que tiene un nombre fuerte (strong name) recibe implícitamente un LinkDemand del conjunto de permisos Full Trust. Esto provoca el que dichos ensamblados no puedan ser llamados desde programas que no tengan confianza completa. Si el programador ha tenido en cuenta al desarrollar el ensamblado las implicaciones de seguridad de los entornos que tienen confianza parcial, puede declarar expresamente la validez de su código en dichos entornos mediante el atributo AllowPartiallyTrustedCallers: La consecuencia de aplicar este atributo es que se elimina el mencionado LinkDemand y el ensamblado deja de requerir que sus llamantes tengan permisos Full Trust.

Conclusión Cuando escribamos librerías que utilicen llamadas a código no gestionado, y sea previsible que éstas puedan ser llamadas desde código que no tenga confianza completa, es recomendable que introduzcamos dentro las peticiones de permisos (con "Demand") que sean oportunas de conformidad con las operaciones que realice dicho código no manejado, a fin de evitar que los programas que llamen a nuestra librería y no tengan confianza completa puedan aprovecharse de ella para saltarse las políticas de seguridad.

blog comments powered by Disqus
autor
referencias