[ASP.NET] Revisa la configuración de ELMAH en producción

Uno de los mayores problemas que tenemos los desarrolladores cuando usamos alguna librería o plugin de terceros es no documentarnos correctamente antes de usar dicho componente. Posteriormente, una vez “probado todo” subimos nuestra aplicación a producción y sin darnos cuenta podemos tener una brecha de seguridad bastante importante, concretamente en el caso de usar mal ELMAH, bastante “gorda”. Esto se acentua con el el uso de Nuget, que es un pedazo de herramienta ojo!, pero a veces nos hace ser vagos o puede ser que no haga todo lo que tenga que hacer y nosotros no lo verifiquemos. Añadimos un paquete desde Visual Studio y por arte de magia todo funciona, pero no nos paramos a revisar que es lo que ha hecho el paquete, ¿Qué ficheros  ha incluido?, ¿Qué configuraciones ha hecho?…

Nota: No recuerdo a partir de que versión del paquete de ELMAH ha sido corregido un pequeño fallo de seguridad de configuración (Vamos a verlo ahora) a la hora de instalar ELMAH a través del paquete Nuget elmah pero ya había mecanismos en ASP.NET para evitar este fallo de seguridad utilizando el elemento location en el web.cofig, al fin y al cabo ELMAH usa un handler axd que podemos securizar.

Demo

Lo primero será crear una proyecto web que usaremos como ejemplo:

image

Un sitio MVC como muestra la siguiente imagen:

image

Voy a publicarlo en un WebSite de Azure para comprobar que ocurre cuando intentamos acceder a ELMAH en remoto:

image

Una vez creado el sitio, añadimos el paquete de Nuget  de ELMAH:

image

image

Una vez instalado, publico la aplicación web e intentamos acceder a la consola de errores de ELMAH en la dirección:

http://elmahdemoseguridad.azurewebsites.net/elmah.axd

image

Como os comentaba antes, el fallo de seguridad debido a una falta de configuración que había antes, ha sido resuelto en las nuevas versiones de ELMAH. Vamos a ver que han modificado y que puede pasar sino tenemos cuidado.

Abrimos el web.config y vamos a la sección de configuración de ELMAH:

image

Si queremos poder acceder remotamente a la consola de ELMAH, debemos establecer el valor del atributo allowRemoteAccess a true o 1 explicitamente o por defecto aunque lo comentemos o borremos seguiremos sin poder acceder. En versiones anteriores de ELMAH creo que esto no sucedía así.

Vamos a actualizar el valor del atributo allowRemoteAccess para simular que es lo que pasaba antes por defecto con ELMAH y que es como actualmente muchos sitios web se encuentran en internet (Luego lo demostraremos). Además añadimos el errorLog de Xml para grabar los errores de ELMAH.

image

Volvemos a acceder a nuestro sitio web:

image

A partir de ahora, cualquier usuario podría acceder a visualizar los errores que se han porducido en el servidor, eso te pasa también si usas incorrectamente el elemento customErrors, aunque para mi lo más preocupante es el tema relacionado con el ROBO DE SESION

Robando la sesión a un usuario

Vamos a modificar la aplicación web para que se produzca un error cuando el usuario visita la página de About 

image

Arrancamos la aplicación y visitamos la página de About y se produce el error

image

Ahora vamos a la consola de ELMAH para ver el error con más detalle:

image

Como podemos observar, el campo User está vacío pues estamos accediendo anónimamente a la aplicación. Si pulsamos sobre el enlace Details… podemos ver con todo detalle la traza de la excepción así como un conjunto de variables de servidor. Nos vamos a quedar con HTTP_COOKIE:

image

Actualmente no hay ninguna cookie de autenticación, así pues vamos a registrarnos en la aplicación con un usuario:

image

Esto nos creará una cuenta nueva y nos redireccionará a la home autenticados:

image

Ahora vamos a volver a navegar a About

image

Volvemos a la consola de ELMAH

image

Ahora si tenemos un usuario en el campo User. Pinchamos sobre el detalle del error y vamos a ver que contiene HTTP_COOKIE:

image

Ahora si está presente .AspNet.ApplicationCookie que es la cookie que genera AspNetIdentity y que ha sido registrada por ELMAH cuando se produjo el error.

Ahora vamos a copiar esta cookie y vamos a usar un plugin de Chrome para manipular cookies que se llama EditThisCookie. Para ello abrimos una nueva ventana en modo incognito y verificamos que no hay nadie con login:

image

Ese icono es EditThisCookie y como bien nos muestra la imagen no hay cookies:

image

Pulsamos sobre el icono + y añadimos la cookie que copiamos de ELMAH:

image

Refrescamos el navegador y voliá!!!

image

Hemos robado la sesión al usuario elmah@demo.es sin unos cracks en seguridad informática.

¡AUN HAY MÁS! Como nos solía recordar el mítico SUPER RATÓN, al ser una página pública, Google indexa la página de ELMAH sino le decimos lo contrario o no la securizamos, así pues podemos consultar a Google sobre sitios que contengan esa Url:

image

Recomendaciones

Es una mala práctica manipular el atributo allowRemoteAccess para poder acceder remotamente ELMAH. Sería recomendable acceder al servidor a través de un escritorio remoto y acceder de manera local.

Siempre tratar de actualizar este tipo de componentes a la última versión y estar al tanto de posible fallos de seguridad que se encuentren.

Utilizar alguna herramienta para revisar la seguridad de nuestros sitios web como esta:

image

Un saludo y happy hacking.

[SignalR] Autenticación JSON Web Token (JWT) en hubs de SignalR

A lo mejor el título no te dice mucho y quizás nunca te hayas encontrado con este caso desarrollando aplicaciones que usan Signalr, así pues voy a tratar de explicar lo mejor posible mi experiencia personal en un caso real con ASP.NET Web Api y Signalr y autenticación JWT.

Si quieres saber que es JWT (JSONWeb Token) te recomiendo este artículo de Atlassian Understanding JWT

 

El escenario

La imagen que a continuación os pongo resume un poco el escenario que me encontré:

image

Un ERP desarrollado en .NET en el cual querían exponer una API REST para que sus clientes pudieran consumir sus servicios. En este caso concreto he puesto un Prestashop que pudiera mostrar el catálogo de productos y poder hacer pedidos a través de esta plataforma desarollada en Php, pero puede ser cualquier otro cliente (SmartPhones, tablets…) o plataforma de terceros (Umbraco, Alfresco…)

Para securizar la API decidimos usar JWT. Para eso expusimos un endpoint llamado /token al cual se le pasa usuario y contraseña, lo valida contra el ERP y devuelve un token JWT como el que os muestro a continuación:

{

    "access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9._N1iZEvnhyE8k9knbwGtJ5IFsPR9Xsv3VXUz8...",

    "token_type":null,

    "expires_in":0,

    "userName":"bsmith",

    "issued":"07/15/2014 07:55:47",

    "expires":"07/29/2014 07:55:47"

}

Con ese token, el cliente puede hacer llamadas a la API añadiendo en cada petición la cabecera Authorization:

image

Hasta aquí todo correcto y funcionando, pero añadimos una capa de complejidad más:

Notificaciones en tiempo real con Signalr

Imaginaros que el cliente da de alta un pedido en Prestashop y para ello hace una llamada a la API. La API a su vez llama al core del ERP para dar de alta el pedido y el ERP contesta con un OK y la API a su vez contesta a Prestashop que OK. Todo esto desata en brackground un proceso/s de negocio en el ERP, pero nuestra API ya contestó a Prestashop y por lo tanto no hay una conexión abierta que indique que está ocurriendo o que ha ocurrido.

El cliente y cuando digo cliente, no me refiero al usuario que está comprando en Prestashop, sino al propietario de Prestashop, quiere poder suscribirse a notificaciones que le permita saber que ocurre con los procesos de negocio del ERP, es decir que el ERP le notifique y no tenga que estar consultando continuamente como se encuentran los pedidos (Por poner un ejemplo) y en base a ello poder actualizar información y actuar.

Para notificar a cada cliente correctamente necesitamos desde la parte servidora saber a quién tenemos que notificar y para ello necesitamos que Signalr asocie las conexiones a cada cliente y aquí es donde entra en juego el token JWT que la API nos emitió. Con este token no sólo sabremos a que cliente tenemos que notificar sino que securizaremos nuestro hub para que nadie sin un token válido pueda acceder a información confifencial de cada cliente.

Mi solución

Lo que os voy a contar ahora, quizás nos sea la mejor manera de hacerlo, pero lo he intentado de otra forma como creando un atributo de autorización personalizado y no he lo he conseguido, por lo menos en este escenario no encajaba. Con Owin y el middleware de de JWT se puede hacer pero en mi caso no usamos este middleware para generar los tokens JWT y por eso tampo encajaba (Sobre esto escribiré otro post más adelante). Por último, intentaré traducir este artículo para recibir feedback del equipo de producto de Signalr por si lo que estoy haciendo está mal y hay mejores maneras de hacerlo.

Lo primero que me puse a investigar es como podía pasar el token JWT a Signalr y gracias a Rui Marinho me enteré que se podía pasar por QueryString usando la propiedad qs del hub:

$(document).ready(function(){

    var hub = $.connection.erpHub;

    $.connection.hub.logging = true;

    $.connection.hub.qs = "Bearer=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1...";

    hub.client.onProccesedOrder = function (order) {

        alert(order.status);

    }

 

    $.connection.hub.start();

});

A través de esa propiedad podéis pasar cualquier parámetro por QueryString a vuestros hubs.

A continuación, necesitamos obtener el token en el hub para validarlo. Para ello sobreescribimos los métodos OnConnected() y OnDisconnected() del hub.

public override Task OnConnected()

{

    return ValidateJwt(Add);

}

 

public override Task OnDisconnected()

{

    return ValidateJwt(Remove);

}

 
La firma del método ValidateJwt recibe un Func<string, Taks> por temas de refactorización y evitar código duplicado porque los métodos de OnConnected() y OnDisconnected() solo difieren en una línea de código como ahora veremos. (Aquí agradezco la ayuda de mi compañero Andrés aunque tenemos pendiente una refactorización usando el patrón estrategia que creo quedará más claro de leer, pero lo importante era evitar código duplicado)
 
private Task ValidateJwt(Func<string, Task> execute)

{

    var bearer = Context.QueryString.Get("Bearer");

 

    if (String.IsNullOrWhiteSpace(bearer))

    {

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Unauthorized));

    }

 

    var principal = CreateClaimsPrincipalFrom(bearer);

 

    if (principal == null || principal.Identity == null || !principal.Identity.IsAuthenticated)

    {

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Unauthorized));

    }

 

    SetContext(principal);

 

    var userName = Context.User.Identity.Name;

 

    return execute(userName);

}

Los métodos Add y Remove sólo se encargan de añadir la empresa a un grupo para poder comunicarnos con ella (ahora en el ejemplo completo podréis verlos) pero realmente importante es el siguiente método:

private void SetContext(ClaimsPrincipal principal)

{

    Context.Request.Environment["server.User"] = principal;

    Context = new HubCallerContext(

        new ServerRequest(Context.Request.Environment), Context.ConnectionId);

}

Lo que estamos haciendo básicamente es crear un contexto nuevo con los claims que han sido generadas a partir de la validación del token JWT que pasamos al Hub por QueryString y así cuando accedamos a Context.User.Identity.Name tendremos el nombre de la empresa que venía en los claims.

El código completo del hub es este:

public class ErpHub : Hub

{

    public override Task OnConnected()

    {

        return ValidateJwt(Add);

    }

 

    public override Task OnDisconnected()

    {

        return ValidateJwt(Remove);

    }

 

    private Task Add(string groupName)

    {

        Groups.Add(Context.ConnectionId, groupName);

 

        return base.OnConnected();

    }

 

    private Task Remove(string groupName)

    {

        Groups.Remove(Context.ConnectionId, groupName);

 

        return base.OnDisconnected();

    }

 

    private Task ValidateJwt(Func<string, Task> execute)

    {

        var bearer = Context.QueryString.Get("Bearer");

 

        if (String.IsNullOrWhiteSpace(bearer))

        {

            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Unauthorized));

        }

 

        var principal = CreateClaimsPrincipalFrom(bearer);

 

        if (principal == null || principal.Identity == null || !principal.Identity.IsAuthenticated)

        {

            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Unauthorized));

        }

 

        SetContext(principal);

 

        var userName = Context.User.Identity.Name;

 

        return execute(userName);

    }

 

    private ClaimsPrincipal CreateClaimsPrincipalFrom(string bearer)

    {

        var securityTokenBuilder = new SecurityTokenBuilder();

 

        var tokenValidationParameters = new TokenValidationParameters

        {

            AllowedAudience = Constants.AllowedAudience,

            SigningToken = securityTokenBuilder.CreateFromCertificate(Constants.CertificateSubjectName),

            ValidIssuer = Constants.Issuer

        };

 

        var tokenString = bearer;

        var tokenHandler = CreateTokenHandler();

        var token = CreateToken(tokenString);

        var principal = tokenHandler.ValidateToken(token, tokenValidationParameters);

        return principal;

    }

 

    protected virtual IJwtSecurityToken CreateToken(string tokenString)

    {

        return new JwtSecurityTokenAdapter(tokenString);

    }

 

    protected virtual IJwtSecurityTokenHandler CreateTokenHandler()

    {

        return new JwtSecurityTokenHandlerAdapter();

    }

 

    private void SetContext(ClaimsPrincipal principal)

    {

        Context.Request.Environment["server.User"] = principal;

        Context = new HubCallerContext(

            new ServerRequest(Context.Request.Environment), Context.ConnectionId);

    }

}

 

La demo

Llamamos al hub pasándole el token JWT por QueryString y como podemos observar accedemos a el:

image

Después validarlo y de establecer el contexto nuevo ya podemos acceder al nombre del cliente:

image

Y por último lo añadimos al grupo para poder enviarle notificaciones:

image

Ahora desde el servidor podemos mandar notificaciones a bsmith:

var userName = ClaimsPrincipal.Current.Identity.Name;

var hub = GlobalHost.ConnectionManager.GetHubContext<ErpHub>();

hub.Clients.Group(userName).onProccesedOrder(new Order(){Status = "Cancelado"});

image

Como os he comentado, esta ha sido mi manera de solucionar este caso concreto y quería compartirlo para a ser posible recibir feedback de cualquier tipo así como mejores implementaciones.

Me queda escribir un post comentando como hacer esto mismo usando si usamos el middleware de Owin JwtBearerAuthentication

Un saludo.

[ASP.NET] Cuidado con los archivos que subes a tu servidor web.

Hace tiempo que me ronda este post en la cabeza y nunca me he decidido a escribirlo. Hoy he sacado un poco de tiempo y he montado un ejemplo sobre lo peligroso que puede ser el no tratar con mucho cuidado la subida de archivos al servidor desde una aplicación web, en este caso ASP.NET WebForms, pero esto aplica también a MVC.

Código fuente:

https://github.com/lurumad/upload-files-vulnerability

La aplicación que he montado es supersencilla. Un página Default.aspx con un formulario que sube un archivo a un directorio que se llama Public. Imaginad que esta página es la página del perfil de usuario, donde cada usuario puede seleccionar su foto. Lo correcto sería restringir la extensión de los archivos a subir de tipo imagen (jpeg, jpg, png…), pero imaginad que el programador ha pasado por alto esta tarea no pensando lo que puede ocurrir porque no conoce que lo que está en el root del sitio web se ejecuta :p.

Ahora, un usuario un poco espabilado, le da por copiar la url de la foto de su pefil y se da cuenta que estamos subiendo dicha foto a una carpeta pública que se llama también Public, entonces dicho usuario se decide a probar si en dicho formulario de subida se está controlando el tipo de archivo que se sube. Se crea una página llamada HackPage.aspx junto con su archivo de Code Behind HackPage.aspx.cs y lo sube al servidor (Estos archivos los tenéis en el proyecto para que hagáis pruebas):

image

HackPage.aspx.cs lee el contenido del fichero Web.Config y lo muestra como fichero xml.

public partial class HackPage : System.Web.UI.Page

{

    protected void Page_Load(object sender, EventArgs e)

    {

        var serverPath = Server.MapPath("/");

 

        Response.Clear();

        Response.ClearHeaders();

        Response.ClearContent();

        Response.Buffer = true;

        Response.ContentType = "text/xml";

        Response.BinaryWrite(File.ReadAllBytes(Path.Combine(serverPath, "Web.Config")));

        Response.End();

    }

}

Teclea la url:

http://localhost:56023/public/hackpage

y… ¿Que creéis que ocurrirá?

image

Ojo! Esto es una prueba en local, pero estoy seguro que haciendo una búsqueda por Google no tardaría mucho en encontrar webs con este fallo de seguridad y por supuesto, esto no es una vulnerabilidad de ASP.NET, Tú has introducido una vulnerabilidad en tu aplicación web por no validar la subida de archivos a tu servidor.

Recomendaciones

  1. Restringir el tipo de archivos que el usuario puede subir en nuestra aplicación.
  2. Mover la carpeta de subida de ficheros fuera del root de la aplicación web. Siempre pudes usar un storage alternativo como SkyDrive, Dropbox, Azure Storage, Amazon S3…

Un saludo y Happy Coding!

[ASP.NET] Recuerdalo siempre: En producción customErrors=On

Ya se que puede parecer un poco cansino y repetitivo este tipo de post. ¿Quién no sabe a día de hoy el significado de este atributo en el web.config de una aplicación web verdad? Pues parece ser que mucha gente lo desconoce y desconoce también las implicaciones que conlleva dejarlo en producción con el valor Off, que ha sido posiblemente heredado de el entorno de desarrollo o pre-producción.

Hace tiempo que escribí unos artículo para evitar este tipo de errores. Uno de ellos es el siguiente

Evitar problemas de configuración (Web.Config) en nuestros servidores de producción

Yo me quedo con el dicho eso de que los trapos sucios se lavan en casa, y porque digo esto, pues es más que evidente que el error mostrará información detallada de lo que está pasando (versión de .NET por ejemplo), ya no sólo que comprometará la seguridad de nuestra aplicación en muchos casos (Un aspecto muy importante a tener en cuenta), sino que mostrará al resto de personas nuestras bondades como programadores…

Para ver que esto es una práctica habitual, basta con ir a Google y buscar por ejemplo:

detalle.aspx?id=

detalle.aspx?cod=

image

image

image

Aquí hay que decir que lo han intantado, pero el case-sensitive les ha jugado una mala pasada.

image

y muchos más…

Así que ya sabes, asegurate cuando hagas un paso a producción que este atributo esté a On (Recomendado) con una página de error personalizada Winking smileo RemoteOnly, pero nunca a Off y si tienes aplicaciones en producción de algún cliente, revisalo antes de que te saquen los colores Smile

Saludos y buen finde a todos!

Tramitación online para administraciones públicas

Ahora que llevo un tiempo trabajando sobre este terreno, me gusta de vez en cuando, darme una vuelta por las diversas instituciones para ver que soluciones ofrecen al ciudadano: Portales, accesibilidad, tecnologías, seguridad, LOPD…

Hoy voy a hablar de un caso concreto (aunque no voy a mencionar el nombre de la institución ni la página web, al fin y al cabo es la competencia) en el que he realizado unos trámites para ver como se comporta la paltaforma y del cual he sacado algunas conclusiones que me gustaría compartir con vosotros:

Seguridad y LOPD

Una de las cosas más importantes que se debe tener en cuenta a la hora de realizar aplicaciones de este tipo, es la seguridad, y sobre todo el cumplimiento de la LOPD, estamos trabajando con información sobre ciudadanos: Datos personales y jurídicos (Denuncias, pagos…) y no puede ser que en el correo eléctrónico de comunicación con el ciudadano venga una dirección url a los detalles de tu trámite sin ni siquiera tener que autenticarte en el portal, es más, el enlace se lo he pasado a mi compañero y lo ve perfectamente.

Yo me pregunto: ¿Hasta que punto las Administraciones Públicas cumplen con la LOPD?

Desarrollo

Luego, en el correo que me envían para comunicarme que mi solicitud ha sido tramitada, me encuentro con otro enlace (muy similar al anterior) para ver el detalle de la actuación que se ha llevado a cabo, el cual me redirige a una página como la siguiente:

image 

Esto en un portal en producción, lo que me dice 2 cosas:

1. Que hacen pocas pruebas del código.

2. Qué muy poca gente ha realizado trámites online.

¿Todavía la gente no sabe la importancia de las pruebas unitarias, de integración y funcionales?

Aparte, podrían haber utilizado un framework más moderno tipo ASP.NET ó ASP.NET MVC y de paso poner una página de error genérica para evitar dar detalles del error.

Esta prueba, ha sido sobre un trámite muy sencillo (Sugerencias) y que consta de 4 pasos, no quiero pensar cuando te pongas con trámites del tipo “Licencia de obras”, “Padrón”… ó cuando nos metamos en temas de pasarela de pago :S

Conclusión

Creo que este tipo de aplicaciones deberían tener un control más exhaustivo por parte de algún organismo encargado del tema, ya que la información que se maneja es bastante delicada.

Un saludo

Microsoft Code Analysis Tool .NET (CAT.NET) v1 CTP

CAT.NET es un Add-in para Visual Studio, qué también podemos utilizar a través de la línea de comandos, para analizar el código de nuestras aplicaciones y detectar vulnerabilidades ya conocidas como Cross Site Scripting, SQL Injection, Process Command Injection, File Canonicalization, Exception Information, LDAP Injection, XPATH Injection y Redirection to User Controlled Site.

Lo he instalado y he decido probarlo con un mini-cutre web site:

catnet

La página UserInfo.aspx es vulnerable a SQL Injection:

protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            User user = new User();
            string _userid = Request.QueryString["userid"];
 
            using (SqlConnection conexion = new SqlConnection(""))
            {
                conexion.Open();
                ///Pruebas SQL Injection CAT.NET
                SqlCommand comando = new SqlCommand();
                comando.CommandText = "SELECT Name, Surname FROM Users WHERE Id = '" + _userid + "'";
                comando.Connection = conexion;
 
                IDataReader reader = comando.ExecuteReader();
 
                if (reader.Read())
                {
                    user.Id = _userid;
                    user.Name = reader.GetString(0);
                    user.Surname = reader.GetString(1);
                }
            }
        }
    }

Y en el Handler File.aspx he puesto el código de la famosa vulnerabilidad de canonicalización de BlogEngine para ver sí también lo detecta:

public void ProcessRequest(HttpContext context)
    {
        if (!string.IsNullOrEmpty(context.Request.QueryString["file"]))
        {
            string fileName = context.Request.QueryString["file"];
            string folder = "/files/";
            if (System.IO.File.Exists(context.Server.MapPath(folder) + fileName))
            {
                context.Response.AppendHeader("Content-Disposition", "attachment; filename=" + context.Server.UrlEncode(fileName));
                context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
                context.Server.Transfer(folder + fileName, false);
            }
            else
            {
                context.Response.Status = "404 Bad Request";
            }
        }
    } 

 

 

Arrancamos CAT.NET:

catnet2

Ejecutamos y esperamos los resultados:

catnet3

Como se puede observar, ha detectado la vulnerabilidad de SQL Injection, indicando las líneas donde se encuentran dichas vulnerabilidades aunque la del Handler no.

Podemos acceder a los ficheros de Reglas de CAT.NET, añadir nuevas reglas, modificar las existentes, habilitarlas/deshabilitarlas…

catnet4

Seguiré investigando y a partir de ahora utilizaré esta herramienta para auditar mí código.

Esto es todo!!!