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.