Inyección de código SQL

La inyección de código SQL es un ataque en el cual se inserta código malicioso en las cadenas que posteriormente se pasan a una instancia de SQL Server para su análisis y ejecución. Todos los procedimientos que generan instrucciones SQL deben revisarse en busca de vulnerabilidades de inyección de código, ya que SQL Server ejecutará todas las consultas recibidas que sean válidas desde el punto de vista sintáctico. Un atacante cualificado y con determinación puede manipular incluso os datos con parámetros.

La forma principal de inyección de código SQL consiste en la inserción directa de código en variables especificadas por el usuario que se concatenan con comandos SQL y se ejecutan. Existe un ataque menos directo que inyecta código dañino en cadenas que están destinadas a almacenarse en una tabla o como metadatos. Cuando las cadenas almacenadas se concatenan posteriormente en un comando SQL dinámico, se ejecuta el código dañino.

El proceso de inyección consiste en finalizar prematuramente una cadena de texto y anexar un nuevo comando. Como el comando insertado puede contener cadenas adicionales que se hayan anexado al mismo antes de su ejecución, el atacante pone fin a la cadena inyectada con una marca de comentario "--". El texto situado a continuación se omite en tiempo de ejecución.

En el siguiente script se muestra una sencilla inyección de código SQL. El script crea una consulta SQL concatenando cadenas no modificables con una cadena especificada por el usuario:

var Shipcity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

Se le pide al usuario que escriba el nombre de una ciudad. Si el usuario especifica Redmond, la consulta ensamblada por el script presenta un aspecto similar al siguiente:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond'

Sin embargo, suponga que el usuario especificase lo siguiente:

Redmond'; drop table OrdersTable--

En este caso, la siguiente consulta la ensambla el script:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'

El punto y coma (;) denota el final de una consulta y el principio de otra. El guión doble (--) indica que el resto de la línea actual es un comentario y debe omitirse. Si el código modificado es sintácticamente correcto, el servidor lo ejecutará. Cuando SQL Server procese esta instrucción, SQL Server seleccionará en primer lugar todos los registros de OrdersTable donde ShipCity sea Redmond. A continuación, SQL Server quitará OrdersTable.

Siempre y cuando el código SQL inyectado sea sintácticamente correcto, no será posible detectar alteraciones mediante programación. Por ello, debe validar todos los datos especificados por el usuario y revisar cuidadosamente el código que ejecute comandos SQL construidos en el servidor que utilice. Las prácticas recomendadas de codificación se describen en las siguientes secciones de este tema.

Validar todos los datos especificados por el usuario

Valide siempre los datos especificados por el usuario mediante comprobaciones de tipo, longitud, formato e intervalo. A la hora de implementar medidas de precaución frente a la especificación de datos dañinos, tenga en cuenta la arquitectura y los escenarios de implementación de la aplicación. Recuerde que los programas diseñados para ejecutarse en un entorno seguro pueden copiarse en un entorno no seguro. Las sugerencias que se muestran a continuación deben considerarse prácticas recomendadas:

  • No haga suposiciones sobre el tamaño, tipo o contenido de los datos que recibirá la aplicación. Por ejemplo, debe hacer la siguiente evaluación:

    • Cómo se comportará la aplicación si un usuario (malicioso o no) especifica un archivo MPEG de 10 megabytes cuando la aplicación espera un código postal.

    • Cómo se comportará la aplicación si se incrusta una instrucción DROP TABLE en un campo de texto.

  • Compruebe el tamaño y el tipo de los datos especificados y aplique unos límites adecuados. Esto puede impedir que se produzcan saturaciones deliberadas del búfer.

  • Compruebe el contenido de las variables de cadena y acepte únicamente valores esperados. Rechace las especificaciones que contengan datos binarios, secuencias de escape y caracteres de comentario. Esto puede impedir la inyección de scripts y puede servir de protección frente a explotaciones de saturación del búfer.

  • Cuando trabaje con documentos XML, valide todos los datos con respecto a su esquema a medida que se vayan indicando.

  • No cree nunca instrucciones Transact-SQL directamente a partir de datos indicados por el usuario.

  • Utilice procedimientos almacenados para validar los datos indicados por el usuario.

  • En entornos de varios niveles, todos los datos deben validarse antes de que se admitan en la zona de confianza. Los datos que no superen el proceso de validación deben rechazarse, y debe devolverse un error al nivel anterior.

  • Implemente varias capas de validación. Las precauciones que tome contra usuarios malintencionados ocasionales pueden resultar ineficaces contra piratas informáticos con determinación. Lo más recomendable es validar los datos especificados por el usuario en la interfaz de usuario y, después, en todos los puntos posteriores en que atraviesen un límite de confianza.

    Por ejemplo, la validación de datos en una aplicación de cliente puede evitar la inyección de scripts. Sin embargo, si en el siguiente nivel se asume que ya se ha validado la entrada, cualquier usuario malintencionado que sea capaz de eludir un cliente puede disfrutar de un acceso sin restricciones a un sistema.

  • No concatene nunca datos especificados por el usuario que no se hayan validado. La concatenación de cadenas es el punto de entrada principal de una inyección de scripts.

  • No acepte las siguientes cadenas en campos a partir de los que puedan construirse nombres de archivo: AUX, CLOCK$, COM1 a COM8, CON, CONFIG$, LPT1 a LPT8, NUL y PRN.

Si es posible, rechace los datos que contengan los siguientes caracteres.

Carácter de entrada

Significado en Transact-SQL

;

Delimitador de consultas.

'

Delimitador de cadenas de datos de caracteres.

--

Delimitador de comentarios.

/* ... */

Delimitadores de comentarios. El servidor no evalúa el texto incluido entre /* y */.

xp_

Se utiliza al principio del nombre de procedimientos almacenados extendidos de catálogo, como xp_cmdshell.

Usar parámetros SQL con seguridad de tipos

La colección Parameters de SQL Server proporciona comprobación de tipos y validación de longitud. Si utiliza la colección Parameters, la entrada se considerará un valor literal en lugar de código ejecutable. Otra de las ventajas del uso de la colección Parameters es que puede exigir comprobaciones de tipos y de longitud. Los valores que no estén comprendidos en el intervalo desencadenarán una excepción. En el siguiente fragmento de código se muestra cómo utilizar la colección Parameters:

SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
     SqlDbType.VarChar, 11);
parm.Value = Login.Text;

En este ejemplo, el parámetro @au_id se considera un valor literal en lugar de código ejecutable. Se comprueba el tipo y la longitud de este valor. Si el valor de @au_id no cumple las restricciones especificadas de tipo y longitud, se producirá una excepción.

Usar una entrada con parámetros con los procedimientos almacenados

Los procedimientos almacenados pueden ser susceptibles de una inyección de código SQL si utilizan una entrada sin filtrar. Por ejemplo, el código que se muestra a continuación es vulnerable:

SqlDataAdapter myCommand = 
new SqlDataAdapter("LoginStoredProcedure '" + 
                               Login.Text + "'", conn);

Si utiliza procedimientos almacenados, deberá utilizar parámetros como entrada para dichos procedimientos.

Usar la colección Parameters con SQL dinámico

Si no puede utilizar procedimientos almacenados, puede seguir utilizando parámetros, tal y como se muestra en el siguiente ejemplo de código.

SqlDataAdapter myCommand = new SqlDataAdapter(
"SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SQLParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id", 
                        SqlDbType.VarChar, 11);
Parm.Value = Login.Text;

Filtrar la entrada

Filtrar la entrada también puede ser útil para protegerse frente a inyecciones de código SQL mediante la eliminación de los caracteres de escape. Sin embargo, debido al gran número de caracteres que pueden presentar problemas, no se trata de una defensa confiable. El siguiente ejemplo busca el delimitador de cadenas de caracteres.

private string SafeSqlLiteral(string inputSQL)
{
  return inputSQL.Replace("'", "''");
}

Cláusulas LIKE

Tenga en cuenta que si utiliza una cláusula LIKE, los caracteres comodín seguirán necesitando utilizar caracteres de escape:

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

Revisar el código para la inyección de código SQL

Debe revisar todos los códigos que llaman a EXECUTE, EXEC o sp_executesql. Puede utilizar consultas similares a las siguientes para ayudarle a identificar los procedimientos que contienen estas instrucciones. Esta consulta comprueba si hay 1, 2, 3 ó 4 espacios después de las palabras EXECUTE o EXEC.

SELECT object_Name(id) FROM syscomments

WHERE UPPER(text) LIKE '%EXECUTE (%'

OR UPPER(text) LIKE '%EXECUTE  (%'

OR UPPER(text) LIKE '%EXECUTE   (%'

OR UPPER(text) LIKE '%EXECUTE    (%'

OR UPPER(text) LIKE '%EXEC (%'

OR UPPER(text) LIKE '%EXEC  (%'

OR UPPER(text) LIKE '%EXEC   (%'

OR UPPER(text) LIKE '%EXEC    (%'

OR UPPER(text) LIKE '%SP_EXECUTESQL%'

Incluir parámetros en QUOTENAME() y REPLACE()

En cada procedimiento almacenado seleccionado, compruebe que todas las variables que se utilizan en Transact-SQL dinámico se han administrado correctamente. Los datos procedentes de los parámetros de entrada del procedimiento almacenado o que se han leído desde una tabla deben estar incluidos en QUOTENAME() o REPLACE(). Recuerde que el valor de @variable que se pasa a QUOTENAME() es de tipo sysname, y tiene una longitud máxima de 128 caracteres.

@variable

Contenedor recomendado

Nombre de un asegurable

QUOTENAME(@variable)

Cadena de ≤ 128 caracteres

QUOTENAME(@variable, '''')

Cadena de > 128 caracteres

REPLACE(@variable,'''', '''''')

Cuando se utiliza esta técnica, se pueden revisar las instrucciones SET como se indica a continuación:

--Before:

SET @temp = N'select * from authors where au_lname='''

+ @au_lname + N''''

--After:

SET @temp = N'select * from authors where au_lname='''

+ REPLACE(@au_lname,'''','''''') + N''''

Inyección habilitada mediante truncamiento de datos

Cualquier Transact-SQL dinámico que esté asignado a una variable se truncará si es mayor que el búfer asignado para dicha variable. Un atacante que pueda forzar el truncamiento de instrucciones pasando cadenas largas de forma inesperada a un procedimiento almacenado puede manipular el resultado. Por ejemplo, el procedimiento almacenado creado por el siguiente script es vulnerable a la inyección habilitada mediante truncamiento.

CREATE PROCEDURE sp_MySetPassword

@loginname sysname,

@old sysname,

@new sysname

AS

-- Declare variable.

-- Note that the buffer here is only 200 characters long.

DECLARE @command varchar(200)

-- Construct the dynamic Transact-SQL.

-- In the following statement, we need a total of 154 characters

-- to set the password of 'sa'.

-- 26 for UPDATE statement, 16 for WHERE clause, 4 for 'sa', and 2 for

-- quotation marks surrounded by QUOTENAME(@loginname):

-- 200 – 26 – 16 – 4 – 2 = 154.

-- But because @new is declared as a sysname, this variable can only hold

-- 128 characters.

-- We can overcome this by passing some single quotation marks in @new.

SET @command= 'update Users set password=' + QUOTENAME(@new, '''') + ' where username=' + QUOTENAME(@loginname, '''') + ' AND password = ' + QUOTENAME(@old, '''')

-- Execute the command.

EXEC (@command)

GO

Al pasar 154 caracteres a un búfer de 128 caracteres, un atacante puede establecer una nueva contraseña para sa sin conocer la antigua.

EXEC sp_MySetPassword 'sa', 'dummy', '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''

Por este motivo, debe utilizar un búfer relativamente grande para una variable de comandos o para ejecutar directamente Transact-SQL dinámico dentro de la instrucción EXECUTE.

Truncamiento cuando se utilizan QUOTENAME(@variable, '''') y REPLACE()

Las cadenas que devuelven QUOTENAME() y REPLACE() se truncarán sin avisar si exceden el espacio asignado. El procedimiento almacenado que se crea en el siguiente ejemplo muestra lo que puede suceder.

CREATE PROCEDURE sp_MySetPassword

@loginname sysname,

@old sysname,

@new sysname

AS

-- Declare variables.

DECLARE @login sysname

DECLARE @newpassword sysname

DECLARE @oldpassword sysname

DECLARE @command varchar(2000)

-- In the following statements, the data stored in temp variables

-- will be truncated because the buffer size of @login, @oldpassword,

-- and @newpassword is only 128 characters, but QUOTENAME() can return

-- up to 258 characters.

SET @login = QUOTENAME(@loginname, '''')

SET @oldpassword = QUOTENAME(@old, '''')

SET @newpassword = QUOTENAME(@new, '''')

-- Construct the dynamic Transact-SQL.

-- If @new contains 128 characters, then @newpassword will be '123... n

-- where n is the 127th character.

-- Because the string returned by QUOTENAME() will be truncated,

-- it can be made to look like the following statement:

-- UPDATE Users SET password ='1234. . .[127] WHERE username=' -- other stuff here

SET @command = 'UPDATE Users set password = ' + @newpassword

+ ' where username =' + @login + ' AND password = ' + @oldpassword;

-- Execute the command.

EXEC (@command)

GO

Por ello, la siguiente instrucción establecerá las contraseñas de todos los usuarios en el valor que se pasó en el código anterior.

EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'

Puede forzar el truncamiento de cadenas al exceder el espacio en el búfer asignado cuando utiliza REPLACE(). El procedimiento almacenado que se crea en el siguiente ejemplo muestra lo que puede suceder.

CREATE PROCEDURE sp_MySetPassword

@loginname sysname,

@old sysname,

@new sysname

AS

-- Declare variables.

DECLARE @login sysname

DECLARE @newpassword sysname

DECLARE @oldpassword sysname

DECLARE @command varchar(2000)

-- In the following statements, data will be truncated because

-- the buffers allocated for @login, @oldpassword and @newpassword

-- can hold only 128 characters, but QUOTENAME() can return

-- up to 258 characters.

SET @login = REPLACE(@loginname, '''', '''''')

SET @oldpassword = REPLACE(@old, '''', '''''')

SET @newpassword = REPLACE(@new, '''', '''''')

-- Construct the dynamic Transact-SQL.

-- If @new contains 128 characters, @newpassword will be '123...n

-- where n is the 127th character.

-- Because the string returned by QUOTENAME() will be truncated, it

-- can be made to look like the following statement:

-- UPDATE Users SET password='1234…[127] WHERE username=' -- other stuff here

SET @command= 'update Users set password = ''' + @newpassword + ''' where username='''

+ @login + ''' AND password = ''' + @oldpassword + '''';

-- Execute the command.

EXEC (@command)

GO

Como en el caso de QUOTENAME(), se puede evitar el truncamiento de cadenas por REPLACE() al declarar variables temporales que sean lo suficientemente grandes para todos los casos. Si es posible, debe llamar a QUOTENAME() o REPLACE() directamente dentro de Transact-SQL dinámico. De lo contrario, puede calcular el tamaño de búfer necesario del siguiente modo. Para @outbuffer = QUOTENAME(@input), el tamaño de @outbuffer debe ser 2*(len(@input)+1). Cuando utilice REPLACE() y las comillas dobles, como en el ejemplo anterior, bastará con un búfer de 2*len(@input).

En el siguiente cálculo se cubren todos los casos:

While len(@find_string) > 0, required buffer size =

round(len(@input)/len(@find_string),0) * len(@new_string)

+ (len(@input) % len(@find_string))

Truncamiento cuando se utiliza QUOTENAME(@variable, ']')

El truncamiento se puede producir cuando el nombre de un asegurable de SQL Server se pasa a las instrucciones que utilizan el formato QUOTENAME(@variable, ']'). Esto se muestra en el ejemplo siguiente.

CREATE PROCEDURE sp_MyProc

@schemaname sysname,

@tablename sysname,

AS

-- Declare a variable as sysname. The variable will be 128 characters.

-- But @objectname actually must allow for 2*258+1 characters.

DECLARE @objectname sysname

SET @objectname = QUOTENAME(@schemaname)+'.'+ QUOTENAME(@tablename)

-- Do some operations.

GO

Cuando se concatenan valores de tipo sysname, se deben utilizar variables temporales que sean lo suficientemente grandes como para comprender el máximo de 128 caracteres por valor. Si es posible, se debe llamar a QUOTENAME() directamente dentro de Transact-SQL dinámico. De lo contrario, puede calcular el tamaño de búfer necesario como se explica en la sección anterior.