Compartir a través de


Inyección de código SQL

Se aplica a: SQL Server Azure SQL Database Azure SQL Managed Instance Azure Synapse Analytics Analytics Platform System (PDW)

La inyección de código SQL es un ataque en el que se inserta código malintencionado en cadenas que posteriormente se pasan a una instancia del motor de base de datos 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 el motor de base de datos 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.

Funcionamiento de la inyección de código SQL

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 malintencionado.

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 malhechor 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 compila una consulta SQL mediante la concatenación de cadenas codificadas de forma rígida junto 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 especifican Redmond, la consulta ensamblada por el script presenta un aspecto similar al siguiente ejemplo:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

Sin embargo, supongamos que el usuario especificase el siguiente texto:

Redmond';drop table OrdersTable--

En este caso, el script ensambla la siguiente consulta:

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 guion 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 ejecuta. Cuando el motor de base de datos procesa esta instrucción, primero selecciona todos los registros en OrdersTable donde ShipCity es Redmond. A continuación, el motor de base de datos quita OrdersTable.

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

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, ten en cuenta la arquitectura y los escenarios de implementación de la aplicación. Recuerda 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 errante o malicioso introduce un archivo de vídeo de 2 GB cuando la aplicación espera un código postal?

    • ¿Cómo se comportará la aplicación si se inserta 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 trabajes con documentos XML, valida 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 tomes 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 del lado 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 concatenes 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.

  • Las cadenas siguientes no se aceptan en campos desde los que se puedan construir nombres de archivo: AUX, CLOCK$, COM1 mediante COM8, CON, CONFIG$, LPT1 mediante 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 del comentario de una sola línea. El servidor no evalúa el texto siguiente -- hasta el final de esa línea.
/*** ... ***/ Delimitadores de comentarios. El servidor no evalúa el texto incluido entre /* y */.
xp_ Se usa 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 del motor de base de datos proporciona comprobación de tipos y validación de longitud. Si usas la colección Parameters, la entrada se considerará como un valor literal en lugar de código ejecutable. Un beneficio adicional de usar la colección Parameters es que puedes exigir comprobaciones de tipos y de longitud. Los valores fuera del intervalo desencadenan una excepción. En el siguiente fragmento de código se muestra cómo usar 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 produce 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 de parámetros con SQL dinámico

Si no puedes usar procedimientos almacenados, de todos modos puedes seguir usando parámetros, tal y como se muestra en el ejemplo de código siguiente.

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;

Filtro de 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, el filtrado no se trata de una defensa fiable. El siguiente ejemplo busca el delimitador de cadenas de caracteres.

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

Cláusulas LIKE

Si usas una cláusula LIKE, los caracteres comodín seguirán necesitando usar 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 todo el código que llama a EXECUTE, EXECo 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 o 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 usan en Transact-SQL dinámico se administran 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(). Recuerda 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 elemento protegible 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 siguiente ejemplo de procedimiento almacenado es vulnerable a la inyección habilitada mediante truncamiento.

En este ejemplo, tenemos un búfer @command con una longitud máxima de 200 caracteres. Necesitamos un total de 154 caracteres para establecer la contraseña de 'sa': 26 para la instrucción UPDATE, 16 para la cláusula WHERE, 4 para 'sa' y 2 para comillas rodeadas por QUOTENAME(@loginname): 200 - 26 - 16 - 4 - 2 = 154. Pero, dado que @new se declara como sysname, esta variable solo puede contener 128 caracteres. Podemos superar esto pasando algunas comillas simples en @new.

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password=' + QUOTENAME(@new, '''')
    + ' WHERE username=' + QUOTENAME(@loginname, '''') 
    + ' AND password=' + QUOTENAME(@old, '''')

-- Execute the command.
EXEC (@command);
GO

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

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

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

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

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

En este ejemplo, los datos almacenados en variables temporales se truncan, ya que el tamaño del búfer de @login, @oldpassword y @newpassword solo tiene 128 caracteres, pero QUOTENAME() puede devolver hasta 258 caracteres. Si @new contiene 128 caracteres, @newpassword puede ser 123... n, donde n es el carácter 127. Dado que la cadena devuelta por QUOTENAME() se trunca, se puede hacer que tenga un aspecto similar a la siguiente instrucción:

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

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);

SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users set password = ' + @newpassword
    + ' WHERE username = ' + @login
    + ' AND password = ' + @oldpassword;

-- Execute the command.
EXEC (@command);
GO

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

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

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

En este ejemplo, los datos se truncan porque los búferes asignados para @login, @oldpassword y @newpassword solo pueden contener 128 caracteres, pero QUOTENAME() puede devolver hasta 258 caracteres. Si @new contiene 128 caracteres, @newpassword puede ser '123...n', donde n es el carácter 127. Dado que la cadena devuelta por QUOTENAME() se trunca, se puede hacer que tenga un aspecto similar a la siguiente instrucción:

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

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);

SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');

-- Construct the dynamic Transact-SQL.
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, debes 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 usa REPLACE() y 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 elemento protegible del motor de base de datos se pasa a las instrucciones que utilizan el formato QUOTENAME(@variable, ']'). En el siguiente ejemplo se muestra este escenario.

En este ejemplo, @objectname debe permitir 2 * 258 + 1 caracteres.

CREATE PROCEDURE sp_MyProc
    @schemaname SYSNAME,
    @tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;

SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
    -- Do some operations.
GO

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