Cómo: Proteger ASP.NET de inyecciones SQL

Mayo de 2005

Publicado: 21 de Diciembre de 2005

patterns & practices Developer Center (en inglés)

Por J.D. Meier, Alex Mackman, Blaine Wastell, Prashant Bansode, Andy Wigley
Microsoft Corporation

Este artículo se aplica a:
ASP.NET versión 1.1
ASP.NET versión 2.0

Resumen: En este artículo se describen una serie de métodos que ayudan a proteger la aplicación ASP.NET de inyecciones SQL. Entre las distintas técnicas se incluyen la restricción de entradas, el uso de parámetros SQL con tipos seguros para el acceso a datos y el uso de una cuenta con menos privilegios que tenga permisos restringidos en la base de datos. La inyección SQL puede producirse cuando una aplicación utiliza entradas para construir instrucciones SQL dinámicas o cuando utiliza procedimientos almacenados para conectarse a la base de datos. Las medidas de seguridad convencionales, como el uso de SSL e IPSec, no protegen a la aplicación de ataques de inyección SQL. Los ataques de inyección SQL que tienen éxito permiten a los usuarios malintencionados ejecutar comandos en la base de datos de la aplicación.

En esta página

Objetivos Objetivos
Descripción general Descripción general
Resumen de los pasos Resumen de los pasos
Paso 1. Restricción de entradas Paso 1. Restricción de entradas
Paso 2. Uso de parámetros con procedimientos almacenados Paso 2. Uso de parámetros con procedimientos almacenados
Paso 3. Uso de parámetros con SQL dinámico Paso 3. Uso de parámetros con SQL dinámico
Factores adicionales Factores adicionales
Comentarios Comentarios
Soporte técnico Soporte técnico
Comunidad y grupos de noticias Comunidad y grupos de noticias
Colaboradores y revisores Colaboradores y revisores

Objetivos

  • Aprender el funcionamiento de los ataques de inyección SQL.

  • Restringir las entradas para evitar las inyecciones SQL.

  • Utilizar parámetros de comando SQL con tipo seguro para evitar las inyecciones SQL.

  • Aprender otras contramedidas para reducir aún más el riesgo

Descripción general

Los ataques de inyección SQL que tienen éxito permiten a los usuarios malintencionados ejecutar comandos en la base de datos de la aplicación utilizando los privilegios concedidos en el inicio de sesión de la aplicación. El problema es más grave si la aplicación utiliza una cuenta con privilegios aumentados para conectarse a la base de datos. Por ejemplo, si en el inicio de sesión de la aplicación se conceden privilegios para eliminar una base de datos, un intruso podría eliminarla en caso de no disponer de las medidas de seguridad oportunas.

Entre las vulnerabilidades habituales que hacen que el código de acceso a datos sea susceptible de ataques de inyección SQL se incluyen:

  • Validación de entradas débil.

  • Construcción dinámica de instrucciones SQL sin utilizar parámetros de tipo seguro.

  • Uso de inicios de sesión en bases de datos con privilegios aumentados.

Ejemplo de inyección SQL

Pensemos en lo que ocurre cuando un usuario introduce la siguiente cadena en el cuadro de texto SSN, en el que se espera que se introduzca un número de la seguridad social con el formato nnn-nn-nnnn.

' ; DROP DATABASE pubs  --

Mediante la entrada, la aplicación ejecuta la siguiente instrucción SQL dinámica o procedimiento almacenado que, a su vez, ejecuta internamente una instrucción SQL similar.

// Use dynamic SQL
SqlDataAdapter myCommand = new SqlDataAdapter(
          "SELECT au_lname, au_fname FROM authors WHERE au_id = '" +
          SSN.Text + "'", myConnection);

// Use stored procedures
SqlDataAdapter myCommand = new SqlDataAdapter(
                                "LoginStoredProcedure '" +
                                 SSN.Text + "'", myConnection);

La intención del desarrollador era que cuando se ejecutase el código, éste insertase la entrada del usuario y generase la siguiente instrucción SQL.

SELECT au_lname, au_fname FROM authors WHERE au_id = '172-32-9999'

Sin embargo, el código inserta la entrada malintencionada del usuario y genera la siguiente consulta.

SELECT au_lname, au_fname FROM authors WHERE au_id = ''; DROP DATABASE pubs --'

En este caso, el carácter ' (comilla simple), con el que comienza la entrada malintencionada, termina el literal de cadena actual de la instrucción SQL. Cierra la instrucción actual sólo si el siguiente símbolo analizado no tiene sentido como continuación de la instrucción actual pero sí como el inicio de una nueva. Como resultado, el carácter de comilla simple de apertura de la entrada malintencionada produce la siguiente instrucción.

SELECT au_lname, au_fname FROM authors WHERE au_id = ''

El carácter ; (punto y coma) indica a SQL que éste es el fin de la instrucción actual, que vendrá seguida del código SQL malintencionado.

; DROP DATABASE pubs

Nota

Los caracteres de punto y coma no son necesariamente obligatorios para separar instrucciones SQL. El uso de este carácter depende del proveedor o de la implementación, pero Microsoft SQL Server no los necesita. Por ejemplo, SQL Server analiza lo siguiente como dos instrucciones distintas:

SELECT * FROM MyTable DELETE FROM MyTable

Por último, la secuencia de caracteres -- (guión doble) es un comentario SQL que indica a SQL que debe ignorar el resto del texto. En tal caso, SQL ignora el carácter ' (comilla simple) de cierre, que en otro caso ocasionaría un error en el analizador SQL.

--'

Instrucciones

Para evitar los ataques de inyección SQL, es necesario:

  • Restringir y sanear los datos de entrada. Para comprobar los datos que se sabe que son buenos, hay que validar el tipo, la longitud, el formato y el intervalo.

  • Utilizar parámetros SQL de tipo seguro para el acceso a los datos. Puede utilizar estos parámetros con procedimientos almacenados o cadenas de comandos SQL construidas dinámicamente. Las colecciones de parámetros como SqlParameterCollection ofrecen la comprobación del tipo y la validación de la longitud. Si se utiliza una colección de parámetros, los datos de entrada se tratan como un valor literal y SQL Server no los trata como código ejecutable. Otra ventaja del uso de colecciones de parámetros es que se pueden realizar comprobaciones de tipo y de longitud. Los valores no comprendidos en el intervalo generan una excepción. Éste es un buen ejemplo de defensa en profundidad.

  • Utilizar una cuenta que disponga de permisos restringidos en la base de datos. Lo ideal sería conceder permisos de ejecución sólo a procedimientos almacenados seleccionados de la base de datos y no ofrecer acceso directo a tablas.

  • Evitar que se revele información de errores de la base de datos. En caso de que se produzcan errores en la base de datos, asegúrese de que no se revelen al usuario mensajes de error detallados.

Nota

Las medidas de seguridad convencionales, como el uso de Secure Socket Layer (SSL) e IP Security (IPSec), no protegen a la aplicación de ataques de inyección SQL.

Resumen de los pasos

Para proteger a la aplicación de inyecciones SQL, realice los siguientes pasos:

  • Paso 1. Restricción de entradas.

  • Paso 2. Uso de parámetros con procedimientos almacenados.

  • Paso 3. Uso de parámetros con SQL dinámico

Paso 1. Restricción de entradas

Se deben validar el tipo, la longitud, el formato y el intervalo de todas las entradas a aplicaciones ASP.NET. Es posible proteger la aplicación de inyecciones SQL mediante la restricción de las entradas utilizadas en las consultas de acceso a datos.

Restricción de entradas en páginas Web ASP.NET

Comience restringiendo las entradas del código en el servidor de las páginas Web ASP.NET. No confíe en la validación del cliente porque no es muy segura. Utilícela sólo para acortar recorridos de ida y vuelta y mejorar la experiencia del usuario.

Si utiliza controles del servidor, emplee los controles del validador de ASP.NET, como RegularExpressionValidator y RangeValidator, para restringir la entrada. Si utiliza controles de entrada HTML normales, utilice la clase Regex en el código del servidor para restringir la entrada.

En el ejemplo de código anterior, si un control TextBox de ASP.NET captura el valor SSN, es posible restringir la entrada mediante el control RegularExpressionValidator, tal y como se muestra a continuación.

<%@ language="C#" %>
<form id="form1" runat="server">
    <asp:TextBox ID="SSN" runat="server"/>
    <asp:RegularExpressionValidator ID="regexpSSN" runat="server"
                                    ErrorMessage="Incorrect SSN Number"
                                    ControlToValidate="SSN"
                                    ValidationExpression="^\d{3}-\d{2}-\d{4}$" />
</form>

Si la entrada SSN procede de otro origen, como un control HTML, un parámetro de cadena de consulta o una cookie, se puede restringir mediante la clase Regex del espacio de nombres System.Text.RegularExpressions. En el siguiente ejemplo se da por sentado que la entrada se ha obtenido de una cookie.

using System.Text.RegularExpressions;

if (Regex.IsMatch(Request.Cookies["SSN"], "^\d{3}-\d{2}-\d{4}$"))
{
    // access the database
}
else
{
    // handle the bad input
}

Para obtener más información sobre la restricción de la entrada en las páginas Web ASP.NET, consulte Cómo: Proteger ASP.NET de los ataques de inyección.

Restricción de entradas en el código de acceso a datos

En algunas situaciones es necesario realizar una validación en el código de acceso a datos, tal vez como complemento a la validación en el nivel de página de ASP.NET. A continuación se presentan dos situaciones habituales en las que es necesario realizar una validación en el código de acceso a datos:

  • Clientes que no sean de confianza. Si los datos pudieran provenir de un origen que no sea seguro o si no puede garantizar que se hayan restringido y validado los datos correctamente, agregue una lógica de validación que restrinja la entrada a las rutinas de acceso a datos.

  • Código de biblioteca. Si el código de acceso a datos está empaquetado como una biblioteca diseñada para que la usen varias aplicaciones, éste debería llevar a cabo su propia validación, ya que no se pueden realizar suposiciones seguras sobre las aplicaciones del cliente.

En el siguiente ejemplo se muestra cómo una rutina de acceso a datos puede validar sus parámetros de entrada mediante expresiones regulares antes de utilizar los parámetros en una instrucción SQL.

using System;
using System.Text.RegularExpressions;

public void CreateNewUserAccount(string name, string password)
{
    // Check name contains only lower case or upper case letters,
    // the apostrophe, a dot, or white space. Also check it is
    // between 1 and 40 characters long
    if ( !Regex.IsMatch(userIDTxt.Text, @"^[a-zA-Z'./s]{1,40}$"))
      throw new FormatException("Invalid name format");

    // Check password contains at least one digit, one lower case
    // letter, one uppercase letter, and is between 8 and 10
    // characters long
    if ( !Regex.IsMatch(passwordTxt.Text,
                      @"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$" ))
      throw new FormatException("Invalid password format");

    // Perform data access logic (using type safe parameters)
    ...
}

Paso 2. Uso de parámetros con procedimientos almacenados

El uso de procedimientos almacenados no implica que se eviten las inyecciones SQL. Lo importante es utilizar parámetros con procedimientos almacenados. Si no utiliza parámetros, los procedimientos almacenados pueden ser susceptibles de inyecciones SQL si utilizan entradas sin filtrar como se describe en el apartado "Descripción general" de este documento.

Con el siguiente código se muestra cómo utilizar SqlParameterCollection cuando se llama a un procedimiento almacenado.

using System.Data;
using System.Data.SqlClient;

using (SqlConnection connection = new SqlConnection(connectionString))
{
  DataSet userDataset = new DataSet();
  SqlDataAdapter myCommand = new SqlDataAdapter(
             "LoginStoredProcedure", connection);
  myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
  myCommand.SelectCommand.Parameters.Add("@au_id", SqlDbType.VarChar, 11);
  myCommand.SelectCommand.Parameters["@au_id"].Value = SSN.Text;

  myCommand.Fill(userDataset);
}

En este caso, el parámetro @au_id es tratado como un valor literal y no como código ejecutable. Además, se comprueba el tipo y la longitud del parámetro. En el ejemplo de código anterior, el valor de entrada no puede tener más de 11 caracteres. Si los datos no cumplen el tipo o la longitud que el parámetro defina, la clase SqlParameter generará una excepción

Revisión del uso de los procedimientos almacenados parametrizados de la aplicación

Ya que el uso de procedimientos almacenados con parámetros no evita necesariamente las inyecciones SQL, es recomendable que revise el uso de este tipo de procedimiento almacenado de la aplicación. Por ejemplo, el siguiente procedimiento almacenado parametrizado tiene varias vulnerabilidades de seguridad.

CREATE PROCEDURE dbo.RunQuery
@var ntext
AS
        exec sp_executesql @var
GO

Una aplicación que utilice un procedimiento almacenado similar al del ejemplo de código anterior tiene las siguientes vulnerabilidades:

  • El procedimiento almacenado ejecuta todas las instrucciones que se le pasen. Consideremos que la variable @var se ha establecido como:

    DROP TABLE ORDERS;
    

    En este caso, la tabla ORDERS será eliminada.

  • El procedimiento almacenado se ejecuta con privilegios dbo.

  • El nombre del procedimiento almacenado (RunQuery) no es muy conveniente. Si un intruso puede consultar la base de datos, verá el nombre del procedimiento almacenado. Con un nombre como RunQuery, puede suponer que es probable que el procedimiento almacenado ejecute la consulta proporcionada.

Paso 3. Uso de parámetros con SQL dinámico

Si no puede utilizar procedimientos almacenados, siga utilizando parámetros cuando construya instrucciones SQL dinámicas. Con el siguiente código se muestra cómo utilizar SqlParameterCollection con SQL dinámico.

using System.Data;
using System.Data.SqlClient;

using (SqlConnection connection = new SqlConnection(connectionString))
{
  DataSet userDataset = new DataSet();
  SqlDataAdapter myDataAdapter = new SqlDataAdapter(
         "SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id",
         connection);
  myCommand.SelectCommand.Parameters.Add("@au_id", SqlDbType.VarChar, 11);
  myCommand.SelectCommand.Parameters["@au_id"].Value = SSN.Text;
  myDataAdapter.Fill(userDataset);
}

Uso de parámetros en el trabajo con lotes

Una creencia errónea habitual es pensar que si se concatenan varias instrucciones SQL para enviar un lote de instrucciones al servidor en un solo recorrido de ida y vuelta, no se pueden utilizar parámetros. Sin embargo, puede utilizar esta técnica siempre que se asegure de que los nombres de parámetros no aparezcan repetidos. Puede estar completamente seguro, utilice nombres de parámetro exclusivos durante la concatenación de texto SQL, tal y como se muestra a continuación.

using System.Data;
using System.Data.SqlClient;
. . .
using (SqlConnection connection = new SqlConnection(connectionString))
{
  SqlDataAdapter dataAdapter = new SqlDataAdapter(
       "SELECT CustomerID INTO #Temp1 FROM Customers " +
       "WHERE CustomerID > @custIDParm; SELECT CompanyName FROM Customers " +
       "WHERE Country = @countryParm and CustomerID IN " +
       "(SELECT CustomerID FROM #Temp1);",
       connection);
  SqlParameter custIDParm = dataAdapter.SelectCommand.Parameters.Add(
                                          "@custIDParm", SqlDbType.NChar, 5);
  custIDParm.Value = customerID.Text;

  SqlParameter countryParm = dataAdapter.SelectCommand.Parameters.Add(
                                      "@countryParm", SqlDbType.NVarChar, 15);
  countryParm.Value = country.Text;

  connection.Open();
  DataSet dataSet = new DataSet();
  dataAdapter.Fill(dataSet);
}
. . .
  

Factores adicionales

Otras cuestiones que hay que tener en cuenta a la hora de desarrollar contramedidas para evitar las inyecciones SQL son las siguientes:

  • Utilizar una cuenta de base de datos con privilegios reducidos.

  • Evitar que se revele información de errores.

Uso de una cuenta de base de datos con privilegios reducidos

La aplicación debería conectar con la base de datos utilizando una cuenta con privilegios reducidos. Si utiliza la autenticación de Windows para conectarse, la cuenta de Windows debería tener menos privilegios desde una perspectiva de sistema operativo y éstos deberían estar limitados; además la cuenta debería tener limitada la capacidad para obtener acceso a los recursos de Windows. De forma adicional, utilice o no la autenticación Windows o la autenticación SQL, el inicio de sesión correspondiente de SQL Server debería estar restringido por permisos en la base de datos.

Por ejemplo, imaginemos este ejemplo: una aplicación ASP.NET que se ejecuta en Microsoft Windows Server 2003 y que obtiene acceso a una base de datos en un servidor distinto del mismo dominio. De forma predeterminada, la aplicación ASP.NET se ejecuta en un grupo de aplicaciones en la cuenta del servicio de red. Esta cuenta dispone de menos privilegios.

Para tener acceso a SQL Server con la cuenta del servicio de red

  1. Cree un inicio de sesión de SQL Server para la cuenta del servicio de red del servidor Web. La cuenta del servicio de red tiene unas credenciales de red que se presentan en el servidor de base de datos como la identidad DOMINIO\NOMBRESERVIDORWEB$. Por ejemplo, si el dominio se llama XYZ y el servidor Web se llama 123, cree un inicio de sesión de la base de datos para XYZ\123$.

  2. Conceda al nuevo inicio de sesión acceso a la base de datos necesaria. Para ello cree un usuario de la base de datos y agréguelo a una función de ésta.

  3. Establezca permisos para que esta función de la base de datos pueda llamar a los procedimientos almacenados necesarios o tener acceso a las tablas necesarias de la base de datos. Conceda el acceso sólo a los procedimientos almacenados que necesite utilizar la aplicación y conceda sólo el acceso suficiente a las tablas en función de los requisitos mínimos de la aplicación.

    Por ejemplo, si la aplicación ASP.NET sólo realiza búsquedas en la base de datos y no actualiza los datos, sólo necesita otorgar acceso de lectura a las tablas. De este modo se limita el daño que un intruso puede causar si tiene éxito en un ataque de inyección SQL.

Prevención de revelado de información de errores

Utilice el control de excepciones estructurado para detectar errores y evitar que se propaguen al cliente. Cree un registro con la información detallada de errores de forma local, pero devuelva al cliente detalles de error limitados.

Si se producen errores mientras el usuario se conecta a la base de datos, asegúrese de que únicamente facilita al usuario información limitada acerca de la naturaleza del error. Si revela información relacionada con errores en el acceso a datos y en bases de datos, podría estar facilitando información útil a un usuario malintencionado que podría utilizar para comprometer la seguridad de la base de datos. Los intrusos utilizan la información de los mensajes de error detallados para desmantelar una consulta SQL a la que intentan inyectar código malintencionado. Un mensaje de error detallado puede revelar información valiosa como la cadena de conexión, el nombre del servidor SQL o las convenciones de nomenclatura de tablas y bases de datos.

Comentarios

Si desea enviar comentarios puede utilizar la wikipedia (en inglés) o el correo electrónico:

Nos interesan especialmente sus comentarios sobre los aspectos siguientes:

  • Problemas técnicos relacionados con nuestras recomendaciones

  • Problemas de aprovechamiento y utilidad

Soporte técnico

Los servicios de soporte de Microsoft ofrecen el soporte técnico de los productos y tecnologías de Microsoft a los que se hace referencia en esta guía. Para obtener información sobre soporte, visite el sitio Web de soporte de Microsoft en http://msdn.microsoft.com/security/default.aspx?pull=/isapi/gosupport.asp?Target=/.

Comunidad y grupos de noticias

El soporte técnico de la comunidad se ofrece en los foros y grupos de noticias:

Para sacar el mayor partido de estos foros, busque el grupo de noticias correspondiente a su tecnología o problema. Por ejemplo, si tiene un problema con las características de seguridad de ASP.NET, debería utilizar el foro de seguridad de ASP.NET (ASP.NET Security).

Colaboradores y revisores

  • Colaboradores y revisores externos: Andy Eunson; Chris Ullman, Content Master; David Raphael, Foundstone Professional Services; Rudolph Araujo, Foundstone Professional Services; Manoranjan M. Paul

  • Colaboradores y revisores de PSS y Microsoft Consulting Services: Wade Mascia, Tom Christian, Adam Semel, Nobuyuki Akama, Microsoft Corporation

  • Colaboradores y revisores de Microsoft Product Group: Stefan Schackow, Vikas Malhotra, Microsoft Corporation

  • Colaboradores y revisores de MSDN: Kent Sharkey, Microsoft Corporation

  • Colaboradores y revisores de Microsoft IT: Eric Rachner, Rob Beck, Shawn Veney (ACE Team), Microsoft Corporation

  • Equipo de pruebas: Larry Brader, Microsoft Corporation; Nadupalli Venkata Surya Sateesh, Sivanthapatham Shanmugasundaram, Sameer Tarey, Infosys Technologies Ltd.

  • Equipo de edición: Nelly Delgado, Microsoft Corporation; Sharon Smith, Tina Burden McGrayne, Linda Werner & Associates Inc.

  • Dirección de publicación: Sanjeev Garg, Microsoft Corporation

Mostrar: