Seguridad en ASP.NET

Protección de las aplicaciones ASP.NET contra hackers

Adam Tuliper

Casi a diario los medios de comunicación masivos informan sobre algún sitio que fue victima de un ataque. Debido a estas constantes intrusiones por grupos de hackers destacados, los desarrolladores se preguntan si estos grupos emplean técnicas avanzadas en sus nefastas actividades. Aunque algunos de los ataques modernos pueden ser muy complejos, los más eficaces a menudo son sorprendentemente sencillos… y se han usado desde hace años. Por fortuna, las medidas para evitar estos ataques por lo general también son sorprendentemente sencillas.

Daré un vistazo a algunos de los tipos de ataques más comunes en el transcurso de dos artículos. En el primer artículo hablaré sobre la inyección SQL y la alteración de parámetros, mientras que en la segunda parte, que aparecerá en la edición de enero, me centraré en el scripting y la suplantación de solicitudes entre sitios.

Y en caso de dudas, si se pregunta si solo los sitios grandes deben preocuparse por los ataques informáticos, la respuesta es muy sencilla: todos los desarrolladores deben preocuparse acerca de los intentos de ataques en sus aplicaciones. Es su tarea proteger las aplicaciones, es lo que esperan los usuarios. En Internet incluso las aplicaciones pequeñas están expuestas a sondeos, debido a la enorme cantidad de programas automatizados disponibles para realizar ataques informáticos. Imagínese que alguien roba su tabla de usuarios o clientes, y que las contraseñas de estas tablas también se usan en otras aplicaciones. Claro, siempre se recomienda que los usuarios usen contraseñas diferentes, pero en la vida real no lo hacen. No va a querer tener que informar a sus usuarios que su aplicación abrió la puerta para un robo de información. Alguien realizó un ataque a un blog pequeñísimo de un amigo mío, sobre su viaje al monte Everest, y borró toda la información, sin ningún motivo aparente. Sin protección, prácticamente nadie está seguro.

A menos que su red esté desconectada físicamente de cualquier dispositivo de comunicaciones externo, existe la posibilidad de que alguien ingrese a la red mediante problemas de configuración de proxy, ataques al Protocolo de escritorio remoto (RDP) o una red privada virtual (VPN), vulnerabilidades de ejecución de código remoto ejecutadas por un usuario interno por visitar simplemente una página web, contraseñas adivinadas, reglas inadecuadas en el firewall, Wi-Fi (gran parte de la seguridad Wi-Fi la puede echar por tierra un atacante ubicado en su estacionamiento), trucos de ingeniería social para que las personas entreguen voluntariamente la información confidencial, y otros métodos de ingreso. A menos que esté completamente desconectado del mundo exterior, nunca puede asumir que un entorno está completamente seguro.

Ahora que lo asusté (eso espero) y se convenció de que la amenaza de los ataques es muy real y que todas sus aplicaciones se pueden sondear, comencemos a entender estos ataques y veamos cómo evitarlos.

Inyección SQL

¿Qué es? La inyección SQL es un ataque en el cual se insertan comandos en una consulta para generar una consulta nueva que el programador no vio. Esto ocurre casi siempre cuando se emplea SQL dinámico; es decir, al concatenar cadenas en el código para generar las instrucciones SQL. La inyección SQL se puede producir en el código .NET Framework de Microsoft al generar una consulta o una llamada a procedimiento y también se puede producir en el código T-SQL del lado del servidor, por ejemplo, en el caso del SQL dinámico en los procedimientos almacenados.

La inyección SQL es especialmente peligrosa, ya que no solo permite solicitar y editar datos, sino que también permite ejecutar comandos en las bases de datos que solo están restringidos por los permisos de los usuarios de las bases de datos o las cuentas de los servicios de las bases de datos. Si su servidor SQL está configurado para que se ejecute con la cuenta de administrador y el usuario de la aplicación pertenece al rol del administrador del sistema, entonces la situación es especialmente preocupante. Los ataques por inyección SQL permiten ejecutar comandos del sistema para realizar las siguientes operaciones:

  • Instalar puertas traseras
  • Transferir toda una base de datos a través del puerto 80
  • Instalar analizadores de redes para robar contraseñas y otros datos confidenciales
  • Descifrar contraseñas
  • Enumerar la red interna; por ejemplo, examinar los puertos de otras máquinas
  • Descargar archivos
  • Ejecutar programas
  • Eliminar archivos
  • Formar parte de una botnet
  • Consultar contraseñas almacenadas en el sistema para completarlas automáticamente
  • Crear usuarios nuevos
  • Crear, eliminar y editar datos; crear y eliminar tablas

Esto no es un listado completo y los límites solo están dados por los permisos implementados y la creatividad del atacante. 

Las inyecciones SQL se conocen hace tanto tiempo, que los programadores frecuentemente me preguntan si siguen siendo un problema. La respuesta es que definitivamente lo son, y son muy comunes. De hecho, después de los ataques de denegación de servicio (DoS), las inyecciones SQL son el tipo de ataque más frecuente.

¿Cómo se realiza? las inyecciones SQL generalmente se aprovechan mediante una entrada directa en una página web o la alteración de parámetros, lo que generalmente implica alterar no solo formularios y URI. También las cookies, los encabezados, y otros están sujetos a estos ataques si la aplicación usa esos valores en una instrucción SQL que no es segura (retomaré la idea más adelante).

Veamos el ejemplo de una inyección SQL mediante la alteración de formularios. Esta es una situación que he visto muchas veces en código de producción. Los detalles de la implementación pueden variar, pero muchos desarrolladores comprueban así las credenciales de inicio de sesión.

Esta es la instrucción SQL creada en forma dinámica para recuperar el inicio de sesión del usuario:

string loginSql = string.Format("select * from users where loginid= '{0}

  ' and password= '{1} '"", txtLoginId.Text, txtPassword.Text);

Esto forma la instrucción SQL:

    select * from dbo.users where loginid='Administrator' and  
    
        password='12345'

En sí no es un problema. Pero supongamos que la entrada en el campo del formulario se parece a lo que vemos en la Figura 1.

Malicious Input Instead of a Valid Username
Figura 1 Entrada malintencionada en vez de un nombre de usuario válido

Esta entrada genera la siguiente instrucción SQL:

    select * from dbo.users where loginid='anything' union select top 1 *
    
      from users --' and password='12345'

Este ejemplo inyecta un identificador de inicio de sesión desconocido “anything”, que por sí mismo no entregaría ningún registro. Pero luego une estos dos resultados con el primer registro en la base de datos, mientras que el símbolo de comentario “--” subsiguiente causa que el resto de la consulta se ignore. Esto no solo permite que el atacante inicie sesión, sino que además pueda devolver el registro de un usuario válido al código que realiza la llamada, sin necesidad de conocer ningún nombre de usuario válido.

Evidentemente, no hace falta que el código replique exactamente esta misma situación; lo importante al analizar las aplicaciones es tener en cuenta el origen de los valores que se incluyen en las consultas. Por ejemplo:

  • Campos de formularios
  • Parámetros de la URL
  • Valores almacenados en la base de datos
  • Cookies
  • Encabezados
  • Archivos
  • Almacenamiento aislado

Es probable que no todos estos casos sean evidentes inmediatamente. Por ejemplo, ¿por qué los encabezados pueden causar problemas? Si la aplicación almacena información de los perfiles de los usuarios en los encabezados y emplea esos valores en consultas dinámicas, entonces se podría producir una vulnerabilidad. Al emplear SQL dinámico se pueden convertir en el origen de ataques.

Sobre todo las páginas web que incluyen funciones de búsqueda pueden ser presa fácil de los atacantes, ya que entregan un canal directo para que realicen las inyecciones.

En una aplicación vulnerable, las funciones que consigue el atacante pueden ser similares a las de un editor de consultas.

Las personas se han vuelto más conscientes de los problemas de seguridad en los últimos años y por lo tanto los sistemas generalmente están más protegidos en forma predeterminada. Por ejemplo, el procedimiento del sistema xp_cmdshell está deshabilitado en las instancias de SQL Server 2005 y las versiones posteriores (inclusive SQL Express). Pero no vaya a creer que por esto los atacantes no tienen cómo ejecutar comandos en su servidor. Si la cuenta que emplea la aplicación para la base de datos tiene un nivel de permisos lo bastante elevado, entonces basta simplemente con que un atacante inyecte el siguiente comando para que vuelva a habilitar la opción:

    EXECUTE SP_CONFIGURE 'xp_cmdshell', '1'

¿Cómo prevenir la inyección SQL? Veamos primero cómo no se soluciona el problema. Un método muy común para reparar las aplicaciones ASP clásicas consistía simplemente en reemplazar los guiones y las comillas. Lamentablemente, en muchas aplicaciones .NET esto sigue siendo el único método de protección.

string safeSql = "select * from users where loginId = " + userInput.Replace("—-", "");

safeSql = safeSql.Replace("'","''");

safeSql = safeSql.Replace("%","");

Este enfoque supone que:

  1. Cada una de las consultas se protegió correctamente con este tipo de llamadas. Depende de que el desarrollador recuerde incluir siempre estas comprobaciones en línea en vez de emplear un patrón que brinde protección en forma predeterminada, incluso después de haber programado todo el fin de semana hasta agotar toda la cafeína.
  2. Se comprobó el tipo de cada uno de los parámetros. Generalmente solo es cuestión de tiempo hasta que un desarrollador olvide comprobar si el parámetro de una página web es por ejemplo un número, para luego usar ese número en una consulta para algo parecido a un ProductId sin realizar las pruebas para las cadenas, ya que, después de todo, es solo un número. Aquí vemos lo que pasa si un atacante cambia el ProductId y éste luego simplemente se lee de la cadena de consulta:
URI: http://yoursite/product.aspx?productId=10

Entrega

  select * from products where productid=10

Y luego el atacante inyecta comandos tales como:

URI: http://yoursite/product.aspx?productId=10;select 1 col1 into #temp; drop table #temp;

Lo que entrega

  select * from products where productid=10;select 1 col1 into #temp; drop table #temp;

¡Oh no! Esto acaba de inyectar en un campo entero que no se filtró con ninguna función de cadena y sin que se comprobara el tipo. Esto se llama un ataque de inyección directa, ya que no se empleó ningún tipo de comillas y la porción inyectada se usa directamente en la consulta sin delimitarla con algún tipo de comillas. Podría responder que “siempre me aseguro de revisar todos los datos”, pero esto carga en el desarrollador la responsabilidad de comprobar manualmente todos los parámetros, lo que abre la puerta para cometer errores. ¿Por qué no usar un patrón mejor para corregir el problema correctamente en toda la aplicación de una vez por todas?

¿Pero cuál es la forma correcta de prevenir la inyección SQL? En la mayoría de las situaciones de acceso a datos es bastante sencillo. La clave está en usar llamadas parametrizadas. De hecho, el SQL dinámico puede ser seguro, siempre que las llamadas estén parametrizadas. Estas son las reglas básicas:

  1. Asegúrese de usar solamente:
    • Procedimientos almacenados (sin SQL dinámico)
    • Consultas parametrizadas (ver Figura 2)

Figura 2 Consulta parametrizada

using (SqlConnection connection = new SqlConnection(  ConfigurationManager.ConnectionStrings[1].ConnectionString))

{

  using (SqlDataAdapter adapter = new SqlDataAdapter())

  {

    // Note we use a dynamic 'like' clause

    string query = @"Select Name, Description, Keywords From Product

                   Where Name Like '%' + @ProductName + '%'

                   Order By Name Asc";

    using (SqlCommand command = new SqlCommand(query, connection))

    {

      command.Parameters.Add(new SqlParameter("@ProductName", searchText));

      // Get data

      DataSet dataSet = new DataSet();

      adapter.SelectCommand = command;

      adapter.Fill(dataSet, "ProductResults");

      // Populate the datagrid

      productResults.DataSource = dataSet.Tables[0];

      productResults.DataBind();

    }

  }

}
  • Llamadas parametrizadas a procedimientos almacenados (ver Figura 3)

Figura 3 Llamada parametrizada a un procedimiento almacenado

//Example Parameterized Stored Procedure Call

string searchText = txtSearch.Text.Trim();

using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings[0].ConnectionString))

{

  using (SqlDataAdapter adapter = new SqlDataAdapter())

  {

    // Note: you do NOT use a query like: 

    // string query = "dbo.Proc_SearchProduct" + productName + ")";

    // Keep this parameterized and use CommandType.StoredProcedure!!

    string query = "dbo.Proc_SearchProduct";

    Trace.Write(string.Format("Query is: {0}", query));

    using (SqlCommand command = new SqlCommand(query, connection))

    {

      command.Parameters.Add(new SqlParameter("@ProductName", searchText));

      command.CommandType = CommandType.StoredProcedure;

      // Get the data.

      DataSet products = new DataSet();

      adapter.SelectCommand = command;

      adapter.Fill(products, "ProductResults");

      // Populate the datagrid.

      productResults.DataSource = products.Tables[0];

      productResults.DataBind();

    }

  }

}
  1.  2.   El SQL dinámico en los procedimientos almacenados se debe componer de llamadas parametrizadas a sp_executesql. Debe evitar el uso de exec, ya que no permite el uso de llamadas parametrizadas. También debe evitar concatenar cadenas con datos proporcionados por los usuarios. Vea la Figura 4.

Figura 4 Llamada parametrizada a sp_executesql

    /*
    
    This is a demo of using dynamic sql, but using a safe parameterized query
    
    */
    
    DECLARE @name varchar(20)
    
    DECLARE @sql nvarchar(500)
    
    DECLARE @parameter nvarchar(500)
    
    /* Build the SQL string one time.*/
    
    SET @sql= N'SELECT * FROM Customer  WHERE FirstName Like @Name Or LastName Like @Name +''%''';
    
    SET @parameter= N'@Name varchar(20)';
    
    /* Execute the string with the first parameter value. */
    
    SET @name = 'm%'; --ex. mary, m%, etc. note: -- does nothing as we would hope!
    
    EXECUTE sp_executesql @sql, @parameter,
    
                          @Name = @name;

Observe que como no admite parámetros, no empleamos exec 'select .. ' + @sql

  1.  3.   No basta con reemplazar los guiones y las comillas y pensar que está a salvo. Debe elegir los métodos de acceso a datos coherentes descritos que previenen la inyección SQL y que no exigen que el programador intervenga manualmente, y debe usarlos religiosamente. Cuando dependemos de una rutina de escape y olvidamos llamarla en alguna parte, podemos ser víctimas de un ataque. Además, existe la posibilidad de que la propia implementación de la rutina de escape pueda tener una vulnerabilidad, por ejemplo un ataque de truncamiento de SQL.
  2.  4.   Debe validar las entradas (consulte la siguiente sección sobre Alteración de parámetros) mediante la comprobación y conversión de tipos; con expresiones regulares para permitir, por ejemplo, solamente valores alfanuméricos o para recuperar datos importantes de fuentes conocidas; no confíe en los datos que vienen de la página web.
  3.  5.   Realice auditorías de los permisos de los objetos de las bases de datos para limitar el ámbito de usuario de las aplicaciones; esto limita la superficie expuesta a los ataques. solo entregue permisos para actualizar, eliminar o insertar cuando el usuario realmente tiene que realizar esas operaciones. Cada aplicación debe tener su propio inicio de sesión con permisos restringidos en la base de datos. Mi aplicación de código abierto SQL Server Permissions Auditor puede ayudar en esta tarea; puede encontrarla en sqlpermissionsaudit.codeplex.com.

Al usar consultas parametrizadas es muy importante auditar los permisos de las tablas. Las consultas parametrizadas requieren que un usuario o un rol tenga permiso para acceder una tabla. La aplicación puede estar protegida contra la inyección SQL, pero ¿qué ocurre si otra aplicación que no está protegida toca la base de datos? Un atacante podría comenzar a realizar consultas en la base de datos, así que deberá asegurarse de que cada aplicación tenga su propio inicio de sesión restringido. También debería auditar los permisos de los objetos de la base de datos, tales como vistas, procedimientos y tablas. Los procedimientos almacenados solo requieren permisos en el procedimiento mismo, no en la tabla; al menos si el procedimiento almacenado no contiene SQL dinámico, lo que facilita la administración de la seguridad. También aquí puede servir mi programa SQL Server Permissions Auditor.

Tenga en cuenta que Entity Framework emplea consultas parametrizadas en segundo plano y por lo tanto está protegido contra la inyección SQL en los contextos de uso más comunes. Algunos desarrolladores prefieren asignar las entidades a procedimientos almacenados en vez de relajar los permisos de las tablas para las consultas parametrizadas dinámicas, pero como ambas partes tienen argumentos válidos, es usted quien debe tomar la decisión. Observe que si usa Entity SQL en forma explícita deberá hacerse cargo de algunas consideraciones adicionales acerca de la seguridad de sus consultas. Consulte la página “Consideraciones de seguridad (Entity Framework)” de MSDN Library en msdn.microsoft.com/library/cc716760.

Alteración de parámetros

¿Qué es? La alteración de parámetros es un ataque en el que se alteran algún tipo de parámetros para cambiar el comportamiento esperado de la aplicación. Los parámetros pueden encontrarse en un formulario, una cadena de consultas, en cookies, bases de datos, etcétera. Aquí analizaré los ataques que implican los parámetros web.

¿Cómo se realiza? Un atacante altera los parámetros para engañar a la aplicación para que realice una acción indebida. Supongamos que para guardar el registro de un usuario leemos el identificador de la cadena de consulta. ¿Esto es seguro? No. Un atacante puede alterar la URL en la aplicación como podemos ver en la Figura 5.

An Altered URL
Figura 5 Dirección URL alterada

Esto permitiría que el atacante cargue en forma indebida la cuenta de un usuario. Y demasiado a menudo las aplicaciones emplean un código como el siguiente, que confía ciegamente en este userId:

// Bad practice!

string userId = Request.QueryString["userId"];

// Load user based on this ID

var user = LoadUser(userId);

¿Existe una forma mejor? Sí, definitivamente. Podemos leer los valores de un origen más confiable, por ejemplo la sesión de un usuario o un proveedor de pertenencia o de perfiles, en vez de confiar en el formulario. 

Existen varias herramientas que permiten alterar más que solo la cadena de consulta. Recomiendo que revise algunas de las barras de herramientas disponibles para los desarrolladores en el explorador web para que vea los elementos ocultos en sus páginas. Creo que se sorprenderá al ver la facilidad con que se pueden alterar los datos. Observe la página “Edit User” que aparece en la Figura 6. Al revelar los campos ocultos de la página podemos ver el identificador del usuario incrustado en el mismo formulario, listo para ser alterado (ver la Figura 7). Este campo se usa como clave principal para el registro del usuario, y al alterarlo se cambia el registro que se vuelve a guardar en la base de datos.

An Edit User Form
Figura 6 Formulario para editar el usuario

Revealing a Hidden Field on the Form
Figura 7 Revelación de un campo oculto en el formulario

¿Cómo prevenir la alteración de los parámetros? No debe confiar en los datos proporcionados por los usuarios, y cuando vaya a emplear los datos en la toma de decisiones, debe validarlos. Por lo general, no nos importa si un usuario altera el segundo nombre almacenado en su perfil. Pero ciertamente nos importa si altera el identificador oculto que representa la clave de su registro de usuario. En estos casos podemos obtener datos confiables de un origen conocido o del servidor en vez de la página web. Podemos almacenar esta información en la sesión del usuario al iniciar sesión o en el proveedor de pertenencia.

Por ejemplo, un método mucho mejor emplea la información del proveedor de pertenencia en vez de usar los datos del formulario:

// Better practice

int userId = Membership.GetUser().ProviderUserKey.ToString();

// Load user based on this ID

var user = LoadUser(userId);

Ahora que ya vimos lo poco confiable que pueden ser los datos del explorador, veamos algunos ejemplos de cómo validar estos datos para ordenar un poco las cosas. Estas son algunas situaciones comunes con los formularios web:

// 1. No check! Especially a problem because this productId is really numeric.

string productId = Request.QueryString["ProductId"];

// 2. Better check

int productId = int.Parse(Request.QueryString["ProductId"]);

// 3.Even better check

int productId = int.Parse(Request.QueryString["ProductId"]);

if (!IsValidProductId(productId))

{

    throw new InvalidProductIdException(productId);

}

En la Figura 8 aparece una situación típica de MVC con un enlace de modelos que realiza conversiones de tipos automáticas sin necesidad de convertir los parámetros en forma explícita.

Figura 8 Enlace de modelos en MVC

[HttpPost]

[ValidateAntiForgeryToken]

public ActionResult Edit([Bind(Exclude="UserId")] Order order)

{

   ...

   // All properties on the order object have been automatically populated and 

   // typecast by the MVC model binder from the form to the model.

   Trace.Write(order.AddressId);

   Trace.Write(order.TotalAmount);

   // We don’t want to trust the customer ID from a page

   // in case it’s tampered with.

   // Get it from the profile provider or membership object

   order.UserId = Profile.UserId;

   // Or grab it from this location

   order.UserId = Membership.GetUser().ProviderUserKey.ToString();

   ...

   order.Save();}

   ...

   // etc.

}

El enlace de modelos es una característica excelente del patrón Controlador de vista de modelo (MVC) que facilita la comprobación de los parámetros, ya que las propiedades del objeto Order se rellenan en forma automática y se convierten en los tipos definidos en función de la información del formulario. También permite definir Anotaciones de datos en los modelos para incluir diferentes validaciones. solo debe cuidar de limitar las propiedades que va a rellenar y, nuevamente, no debe confiar en la página para los elementos importantes. Una buena regla general consiste en tener un ViewModel para cada View, para así poder excluir completamente el UserId del modelo en este ejemplo de Edit.

Observe que uso el atributo [Bind(Exclude)] para limitar los elementos que MVC enlaza en mi modelo, lo que me permite controlar lo que me merece confianza o no. Así me aseguro de que UserId no provenga de los datos del formulario, y por lo tanto no se puede alterar. El enlace de modelos y las Anotaciones de datos están fuera del alcance de este artículo. solo los toqué a vuelo de pájaro para mostrar cómo la tipificación de los parámetros funciona tanto en los formularios Web Forms como en MVC.

Y si por necesidad tiene que incluir un campo con un identificador en el que confía en la página web, puede visitar el vínculo de las Extensiones de seguridad de MVC (mvcsecurity.codeplex.com), donde encontrará un atributo que le servirá para esto.

En resumen

En este artículo presenté dos de las formas más comunes en que las aplicaciones son víctimas de un ataque informático. Pero como puede ver, los ataques se pueden evitar o al menos limitar con unos pocos cambios en las aplicaciones. Evidentemente existen variantes de estos ataques y otras formas para vulnerar la seguridad. En la siguiente entrega abordaré dos tipos más de ataques, el scripting entre sitios y la suplantación de solicitudes entre sitios.   

Adam Tuliper es arquitecto de software en Cegedim y ha desarrollado software durante más de 20 años. Es ponente de la comunidad INETA y periódicamente expone en congresos y grupos de usuarios .NET. Sígalo en Twitter en twitter.com/AdamTuliper, su blog en completedevelopment.blogspot.com o secure-coding.com.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Barry Dorrans