GuilleSQL :: Microsoft SQL Server, SSIS, y más !!

SQL Injection, Procedimientos Almacenados, ADO.Net y SQL Server

Volver a: [SQL Injection]


Este capítulo incluye varios ejemplos de código C# y ADO.Net de una supuesta aplicación ASP.Net, a través de los cuales se analizan las alternativas de desarrollo y programación de SQL Server y ADO.Net desde el punto de vista de SQL Injection. Se introduce parcial y brevemente la programación de bases de datos con ADO.Net, y se explican los beneficios de la utilización de Consultas Parametrizadas y Procedimientos Almacenados invocados de forma Parametrizada, incluyendo código fuente de ejemplo, y analizando los riesgos de SQL Injection en las diferentes alternativas de programación de SQL Server y ADO.Net.

Este artículo no pretende ser un ejemplo a seguir para la programación de aplicaciones (ni de lejos). Mi perfil profesional no es el de un desarrollador, y el hecho de que sea capaz de escribir código fuente que se compile y funcione apropiadamente (más o menos), no implica que yo sea un buen programador, ya que no lo soy (ojalá... pues hace años era uno de mis sueños no cumplidos... en fin). En resumen, el código fuente aquí incluido sólo tiene fines didácticos para el tema que nos ocupa, sin considerar la seguridad, el rendimiento, uso de patrones de diseño u otras consideraciones de diseño, ni ningún otro aspecto que sí debe considerarse en desarrollos profesionales.

Antes de continuar con este capítulo, recordar que en resumidas cuentas, tenemos dos formas de trabajar con Bases de Datos en ADO.Net para conseguir un conjunto de resultados, en función de cómo se gestione la conexión:

  • Trabajar Orientado a la Conexión. En esta forma de trabajo, nos conectaremos a una base de datos (ej: SQL Server), ejecutaremos una consulta SQL o un procedimiento almacenado, y manteniendo la conexión a la base de datos abierta, recorreremos el conjunto o conjuntos de resultados de dicha ejecución, para finalmente cerrar la conexión a la base de datos. En algunos desarrollos, la conexión se mantiene abierta constantemente (esto es típico en antiguas aplicaciones cliente/servidor de Visual Basic, o al menos, yo me lo he encontrado más de una vez).

    Habitualmente suele utilizarse un objeto de la clase SqlCommand en el caso de estar conectado a SQL Server a través de SQLClient, o bien, también suele utilizarse un objeto de la clase OldDbCommand en el caso de estar conectado a un origen de datos OLEDB (ej: SQL Server, Microsoft Access, Oracle, Informix, DB2, etc.). En ambos casos, asociaremos el objeto comando utilizado con un objeto conexión (SqlConnection u OleDbConnection, dependiendo del caso).

    Seguidamente, para recuperar (leer) los datos de nuestra base de datos, suele utilizarse un objeto de la clase SqlDataReader en el caso de estar conectado a SQL Server a través de SQLClient, o bien, también suele utilizarse un objeto de la clase OleDbDataReader en el caso de estar conectado a un origen de datos OLEDB (ej: SQL Server, Microsoft Access, Oracle, Informix, DB2, etc.).

  • Trabajar Orientado a la Desconexión. En esta forma de trabajo, nos conectaremos a una base de datos (ej: SQL Server), ejecutaremos una consulta SQL o un procedimiento almacenado, rellenaremos un objeto en memoria (DataSet) con el conjunto de resultados correspondiente y finalmente cerraremos la conexión a la base de datos, pudiendo acceder al anterior objeto en memoria para recorrer el conjunto de resultados (pero desconectados de la Base de Datos).

    Habitualmente suele utilizarse un objeto de la clase SqlDataAdapter en el caso de estar conectado a SQL Server a través de SQLClient, o bien, también suele utilizarse un objeto de la clase OleDbDataAdapter en el caso de estar conectado a un origen de datos OLEDB (ej: SQL Server, Microsoft Access, Oracle, Informix, DB2, etc.). Tanto en un caso como en otro (SqlDataAdapter u OleDbDataAdapter) se debe especificar el comando (Procedimiento Almacenado, Consulta SQL o Transact-SQL) a ejecutar, ya sea especificando un objeto conexión (SqlConnection u OleDbConnection, dependiendo del caso) y la correspondiente cadena con dicho comando, o bien, especificando un objeto comando (es decir, un objeto SqlCommand u OleDbCommand, dependiendo del caso).

    Seguidamente, suele utilizarse el método Fill del anterior objeto (es decir, del SqlDataAdapter u OleDbDataAdapter, dependiendo del caso), para rellenar un objeto de la clase DataSet, cerrando la conexión al finalizar el llenado del DataSet. De este modo, podremos acceder a dicho DataSet para recuperar (leer) los datos de nuestra base de datos, sin mantener una conexión abierta a la Base de Datos. También pueden modificarse los datos del DataSet, y seguidamente conciliar (sincronizar) dichos cambios en la base de datos (esto queda fuera del alcance del presente artículo).

    En resumidas cuentas, aunque tengamos que utilizar un objeto SqlDataAdapter u OleDbDataAdapter para poder llenar una DataSet con su correspondiente método Fill, en ambos casos estaremos utilizando de forma implícita un objeto de comando (SqlCommand u OleDbCommand), como se hacía al trabajar orientado a la conexión.

Visto lo visto, vemos que podemos trabajar orientados a la conexión o a la desconexión, y también vemos que podemos trabajar con SQLClient (es decir, SQL Server, con las clases SqlConnection, SqlCommand, SqlDataAdapter, etc.) o con OleDb (es decir, con SQL Server o con cualquier otro motor de base de datos del cual tengamos el correspondiente Proveedor OLEDB, utilizando las clases OleDbConnection, OleDbCommand, OleDbDataAdapter, etc.).

Aclarar que con OLEDB también podemos acceder a SQL Server, pero evidentemente será preferible utilizar SQLClient, ya que de esta forma nos quitamos de medio la cada OLEDB, lo cual, resultará más óptimo. Claro está, que muchos prefieren utiliza OLEDB, para que de esta forma sea más rápido modificar sus desarrollos para funcionar contra otros motores de base de datos (yo prefiero para esto, hacer las cosas bien, y utilizar el patrón de diseño DAO: Data Access Objects, ahora para gustos hay colores).

Para los siguientes ejemplos, nos centraremos en la utilización de SQLClient para acceder a SQL Server, ya que en el caso de OLEDB la problemática y la forma de trabajar es la misma (algunas clases son propias de SQLClient o de OleDb, mientras que otras clases como la clase DataSet son comunes).

Del mismo modo, para estudiar SQL Injection no es especialmente importante trabajar orientado a la conexión u orientado a la desconexión, ya que lo verdaderamente importante es la forma en que se accede a la base de datos:

  • Si se utilizan Consultas SQL o Procedimientos Almacenados.
  • Si se utilizan Parámetros o se concatena la información variable.

Como consecuencia de esto, para los siguientes ejemplos nos centraremos en el modo de trabajo orientado a la desconexión (con DataSets), ya que se trabaja orientado a la conexión de forma muy parecida.

En concordancia con los anteriores capítulos de este artículo, hemos desarrollado un ejemplo en el Laboratorio de GuilleSQL con el caso típico de un formulario html para que los usuarios de una supuesta Aplicación ASP.Net se autentiquen, empleando para ello dos cajas de texto (txtUsuario y txtPassword) y un botón. En consecuencia, nos centraremos en el código de servidor que se ejecutará al hacer click sobre dicho botón del formulario html.

Ejecutar Consultas SQL al vuelo con ADO.Net y SQLClient

Este caso ya lo vimos anteriormente con una página ASP, y se trata de un método NO recomendado para acceder a una base de datos desde una aplicación. El código fuente utilizado para este ejemplo es el equivalente a utilizar en java objetos que implementen el interfaz java.sql.Statement.

El código fuente resultante es similar al siguiente:

// Deberemos referenciar los siguientes espacios de nombres
using System.Data;
using System.Data.SqlClient;


// Este es el código del Botón SqlConnection cnSql = new SqlConnection("Data Source=GuilleXP;Initial Catalog=GuilleSQL;trusted_connection=yes;");
String strSQL = "SELECT * FROM GuilleSQL.USUARIOS WHERE USU_ID='" + txtUsuario.Text.ToString() + "' AND USU_PWD='" + txtPassword.Text.ToString() + "'";
SqlDataAdapter miCommand = new SqlDataAdapter(strSQL, cnSql);
miCommand.SelectCommand.CommandType = CommandType.Text;

DataSet miDS = new DataSet();
miCommand.Fill(miDS);

'****
'**** (c) www.guillesql.es
'**** Controlar aquí si existe el Usuario con miDS.Tables[0].Rows.Count en un IF
'**** y tomar los datos del Usuario (desde miDS) si el usuario existe
'****

Al hacer las correspondientes pruebas en el Laboratorio de GuilleSQL, el resultado obtenido ha sido que NO estamos protegidos frente a ataques de SQL Injection, es más, este es el método de acceder a una Base de Datos más vulnerable a ataques SQL Injection. Si un usuario introduce los siguientes valores de entrada:

  • txtUsuario: ' OR 'A'='A
  • txtPassword: ' OR 'A'='A

La ejecución de la correspondiente consulta SQL por parte de la aplicación SI devolverá filas (comprobado en el Laboratorio de GuilleSQL), pues el atacante habrá conseguido su objetivo (debilitar la cláusula WHERE de la consulta SQL ejecutada), como vimos anteriormente en el ejemplo similar con ASP. Lo mismo ocurre con otros tipos de ataque de SQL Injection, es decir, al utilizar deliminadores de comentarios, intentar introducir código SQL invasor, etc, el código se ha comportado de la misma forma.

Así, si por ejemplo, intentamos introducir los siguientes datos de entrada:

  • txtUsuario: ' OR 'A'='A
  • txtPassword: '; DELETE FROM GuilleSQL.USUARIOS; --

El atacante conseguirá eliminar todas las filas de la tabla GuilleSQL.USUARIOS, al haber conseguido introducir el código SQL Invasor deseado (la sentencia DELETE).

Ejecutar Consultas SQL parametrizadas con ADO.Net. y SQLClient

La programación de base de datos con Consultas SQL Parametrizadas no ha sido contemplado anteriormente en este Artículo, resultando un caso de bastante interés (para SQL Injection y en general para la programación de base de datos) cuando se quiere acceder a un motor de base de datos para ejecutar Consultas SQL en vez de ejecutar Procedimientos Almacenados. La principal ventaja frente a la utilización de Consultas SQL al vuelo, es que se evitan las concatenaciones (principal factor de riesgo para SQL Injection) y se utilizan parámetros (validaciones implícitas de tipo y rango). Es equivalente a utilizar en java objetos que implementen el interfaz java.sql.PreparedStatement.

En consecuencia, en caso de no utilizar Procedimientos Almacenados para acceder a la base de datos, si es necesario utilizar Consultas SQL, la recomendación será utilizar las Consultas SQL de forma parametrizada, minimizando así los riesgos.

Finalmente, el código fuente resultante es similar al siguiente:

// Deberemos referenciar los siguientes espacios de nombres
using System.Data;
using System.Data.SqlClient;


// Este es el código del Botón SqlConnection cnSql = new SqlConnection("Data Source=GuilleXP;Initial Catalog=GuilleSQL;trusted_connection=yes;");
SqlDataAdapter miCommand = new SqlDataAdapter("SELECT * FROM GuilleSQL.USUARIOS WHERE USU_ID=@USU_ID AND USU_PWD=@USU_PWD", cnSql);
miCommand.SelectCommand.CommandType = CommandType.Text;

SqlParameter pmReturn = new SqlParameter("@RETURN_VALUE", SqlDbType.Int, 4);
pmReturn.Direction = ParameterDirection.ReturnValue;
miCommand.SelectCommand.Parameters.Add(pmReturn);

SqlParameter pmUsuario = new SqlParameter("@USU_ID", SqlDbType.VarChar, 50);
pmUsuario.Value = txtUsuario.Text.ToString();
miCommand.SelectCommand.Parameters.Add(pmUsuario);

SqlParameter pmPassword = new SqlParameter("@USU_PWD", SqlDbType.VarChar, 50);
pmPassword.Value = txtPassword.Text.ToString();
miCommand.SelectCommand.Parameters.Add(pmPassword);

DataSet miDS = new DataSet();
miCommand.Fill(miDS);

'****
'**** (c) www.guillesql.es
'**** Controlar aquí si existe el Usuario con miDS.Tables[0].Rows.Count en un IF
'**** y tomar los datos del Usuario (desde miDS) si el usuario existe
'****

Al hacer las correspondientes pruebas en el Laboratorio de GuilleSQL, el resultado obtenido ha sido que utilizando Consultas Parametrizadas estamos protegidos frente a ataques de SQL Injection (siempre que el comando ejecutado no incluya a su vez la ejecución de código SQL Dinámico, etc.). Si un usuario introduce los siguientes valores de entrada:

  • txtUsuario: ' OR 'A'='A
  • txtPassword: ' OR 'A'='A

La llamada al procedimiento almacenado no devolverá filas (comprobado en el Laboratorio de GuilleSQL), excepto que realmente existe un usuario con nombre ' OR 'A'='A y contraseña ' OR 'A'='A. Lo mismo ocurre con otros tipos de ataque de SQL Injection, es decir, al utilizar deliminadores de comentarios, intentar introducir código SQL invasor, etc, el código se ha comportado de forma segura.

En el caso particular de SqlDataAdaptar y SqlCommand para acceder a SQL Server, al ejecutar una traza con SQL Profiler, se puede comprobar que el correspondiente objeto SqlCommand generará una llamada al procedimiento almacenado sp_executesql. Así, si por ejemplo, intentamos introducir los siguientes datos de entrada:

  • txtUsuario: ' OR 'A'='A
  • txtPassword: '; DELETE FROM GuilleSQL.USUARIOS; --

No se obtendrá ninguna fila de resultado, y la traza de SQL Profiler revelará la siguiente llamada a sp_executesql:

exec sp_executesql N'SELECT * FROM USUARIOS WHERE USU_ID=@USU_ID AND USU_PWD=@USU_PWD',N'@USU_ID varchar(50),@USU_PWD varchar(50)',@USU_ID=''' or ''a''=''a',@USU_PWD='''; delete from usuarios; --'

Como vemos, el código ejecutado en la base de datos (en el caso del ejemplo, SQL Server), es un código muy lejano del código que deseaba ejecutar el atacante cuando intentó utilizar técnicas de SQL Injection, y además, la forma parametrizada de invocar el procedimiento almacenado sp_executesql es una muestra objetiva de la seguridad de este modo de acceso a base de datos.

Ejecutar Procedimientos Almacenados al vuelo con ADO.Net y SQLClient

Este caso ya lo vimos anteriormente con una página ASP, y se trata de un método NO recomendado para acceder a una base de datos desde una aplicación. Aunque no resulta tan vulnerable como la utilización de Consultas SQL generadas como fruto de concatenaciones, la utilización de Procedimientos Almacenados sin Parametrizar, mantiene la existencia de riesgo de ataques SQL Injection. El código fuente utilizado para este ejemplo es el equivalente a utilizar en java objetos que implementen el interfaz java.sql.Statement.

El código fuente resultante es similar al siguiente:

// Deberemos referenciar los siguientes espacios de nombres
using System.Data;
using System.Data.SqlClient;


// Este es el código del Botón SqlConnection cnSql = new SqlConnection("Data Source=GuilleXP;Initial Catalog=GuilleSQL;trusted_connection=yes;");
String strSQL = "GuilleSQL.spGetUsuario '" + txtUsuario.Text.ToString() + "', '" + txtPassword.Text.ToString() + "'";
SqlDataAdapter miCommand = new SqlDataAdapter(strSQL, cnSql);
miCommand.SelectCommand.CommandType = CommandType.Text;

DataSet miDS = new DataSet();
miCommand.Fill(miDS);

'****
'**** (c) www.guillesql.es
'**** Controlar aquí si existe el Usuario con miDS.Tables[0].Rows.Count en un IF
'**** y tomar los datos del Usuario (desde miDS) si el usuario existe
'****

Al hacer las correspondientes pruebas en el Laboratorio de GuilleSQL, el resultado obtenido ha sido que NO estamos protegidos frente a ataques de SQL Injection. Si un usuario introduce los siguientes valores de entrada:

  • txtUsuario: ' OR 'A'='A
  • txtPassword: ' OR 'A'='A

Se producirá un error en tiempo de ejecución. Si bien, no se ha consegido debilitar la cláusula WHERE de la correspondiente consulta SQL, si es cierto que se ha conseguido un mal funcionamiento de la aplicación, excepto que se tenga programado una correcta gestión de errores (comprobado en el Laboratorio de GuilleSQL).

Por el contrario, si por ejemplo, intentamos introducir los siguientes datos de entrada:

  • txtUsuario: ' OR 'A'='A
  • txtPassword: '; DELETE FROM GuilleSQL.USUARIOS; --

El atacante conseguirá eliminar todas las filas de la tabla GuilleSQL.USUARIOS, al haber conseguido introducir el código SQL Invasor deseado (la sentencia DELETE). Para este tipo de ataques se mantiene completamente la vulverabilidad ante ataques SQL Injection (comprobado en el Laboratorio de GuilleSQL).

Ejecutar Procedimientos Almacenados de forma parametrizada con ADO.Net y SQLClient

Este caso también lo vimos anteriormente con una página ASP, y se trata del método recomendado para acceder a una base de datos desde una aplicación, eso sí, evidentemente, requiere el desarrollo, programación y mantenimiento de los correspondientes Procedimientos Almacenados en SQL Server (o en el motor de base de datos correspondiente). El código fuente utilizado para este ejemplo es el equivalente a utilizar en java objetos que implementen el interfaz java.sql.PreparedStatement.

El código fuente resultante es similar al siguiente:

// Deberemos referenciar los siguientes espacios de nombres
using System.Data;
using System.Data.SqlClient;


// Este es el código del Botón
SqlConnection cnSql = new SqlConnection("Data Source=GuilleXP;Initial Catalog=GuilleSQL;trusted_connection=yes;");
SqlDataAdapter miCommand = new SqlDataAdapter("spGetUsuario", cnSql);
miCommand.SelectCommand.CommandType = CommandType.StoredProcedure;

SqlParameter pmReturn = new SqlParameter("@RETURN_VALUE", SqlDbType.Int, 4);
pmReturn.Direction = ParameterDirection.ReturnValue;
miCommand.SelectCommand.Parameters.Add(pmReturn);

SqlParameter pmUsuario = new SqlParameter("@USU_ID", SqlDbType.VarChar, 50);
pmUsuario.Value = txtUsuario.Text.ToString();
miCommand.SelectCommand.Parameters.Add(pmUsuario);

SqlParameter pmPassword = new SqlParameter("@USU_PWD", SqlDbType.VarChar, 50);
pmPassword.Value = txtPassword.Text.ToString();
miCommand.SelectCommand.Parameters.Add(pmPassword);

DataSet miDS = new DataSet();
miCommand.Fill(miDS);

'****
'**** (c) www.guillesql.es
'**** Controlar aquí si existe el Usuario con miDS.Tables[0].Rows.Count en un IF
'**** y tomar los datos del Usuario (desde miDS) si el usuario existe
'****

Como ya vimos anteriormente, en esta ocasión estamos protegidos ante ataques SQL Injection (siempre y cuando el procedimiento almacenado no utilice SQL Dinámico), ya que no construimos dinámicamente la cadena de texto SQL a ejecutar. En consecuencia, si un usuario introduce los siguientes valores de entrada:

  • txtUsuario: ' OR 'A'='A
  • txtPassword: ' OR 'A'='A

La llamada al procedimiento almacenado no devolverá filas (como hemos comprobado en las pruebas del Laboratorio de GuilleSQL), excepto que realmente existe un usuario con nombre ' OR 'A'='A y contraseña ' OR 'A'='A.

Además, como comentamos anterioremente en el ejemplo realizado con ASP, la utilización de parámetros implica la realización implícita de comprobaciones de tipo y rango. Sin embargo, con ADO.Net existe alguna diferencia frente a ADO. Por ejemplo, si introducimos una cadena demasiado larga en un parámetro de ADO se producirá el error ADODB.Command (0x800A0D5D), mientras que por el contrario, al introducir una cadena demasiado larga en un Parámetro de ADO.Net no se producirá ningún error, aunque la cadena será truncada (esto puede comprobarse fácilmente con una traza de SQL Profiler, es decir, la prueba del algodón ;-).

Conclusiones

Con todo lo visto hasta ahora, tanto en este capítulo como en los capítulos anteriores del presente artículo, para protegernos de SQL Injection, es recomendable realizar una correcta programación de acceso a base de datos, sin utilizar SQL Dinámico, pudiendo elegir principalmente entre:

  • Utilizar Procedimientos Almacenados de forma parametrizada.
  • Utilizar Consultas SQL de forma parametrizada.

En caso contrario, y como hemos podido comprobar de forma empírica durante las pruebas realizadas en el Laboratorio de GuilleSQL, seremos vulnerables ante ataques SQL Injection.

Volver a: [SQL Injection]




Miembros de
Miembros de GITCA (Global IT Community Association)

Menu de Usuario
  Iniciar Sesión
  Registrarse
  Restablecer Contraseña
  Ventajas de Registrarse

Acerca de
  Contigo desde Oct 2007
  771 usuarios registrados
  86146 pageloads/mes
  Ranking Alexa 498160

Social Networks
Sigue a Portal GuilleSQL en Linkedin !!
Sigue a Portal GuilleSQL en Twitter !!



Archivo

Marzo de 2019 (1)
Octubre de 2018 (1)
Julio de 2018 (1)
Junio de 2018 (4)
Mayo de 2018 (5)
Abril de 2018 (3)
Marzo de 2018 (2)
Febrero de 2018 (7)
Enero de 2018 (1)
Diciembre de 2017 (15)
Noviembre de 2017 (7)
Junio de 2017 (3)
Mayo de 2017 (1)
Marzo de 2017 (3)
Enero de 2017 (4)
Junio de 2016 (1)
Mayo de 2016 (2)
Abril de 2016 (2)
Septiembre de 2015 (2)
Agosto de 2015 (2)
Junio de 2015 (10)
Mayo de 2015 (4)
Abril de 2015 (8)
Marzo de 2015 (11)
Octubre de 2014 (3)
Septiembre de 2014 (7)
Agosto de 2014 (5)
Julio de 2014 (2)
Mayo de 2014 (4)
Abril de 2014 (4)
Marzo de 2014 (4)
Febrero de 2014 (1)
Enero de 2014 (5)
Diciembre de 2013 (8)
Noviembre de 2013 (2)
Octubre de 2013 (7)
Septiembre de 2013 (6)
Agosto de 2013 (1)
Julio de 2013 (6)
Junio de 2013 (11)
Mayo de 2013 (7)
Abril de 2013 (6)
Febrero de 2013 (5)
Enero de 2013 (7)
Diciembre de 2012 (12)
Noviembre de 2012 (13)
Octubre de 2012 (5)
Septiembre de 2012 (3)
Agosto de 2012 (6)
Julio de 2012 (4)
Junio de 2012 (1)
Mayo de 2012 (2)
Abril de 2012 (7)
Marzo de 2012 (16)
Febrero de 2012 (9)
Enero de 2012 (5)
Diciembre de 2011 (10)
Noviembre de 2011 (10)
Octubre de 2011 (4)
Septiembre de 2011 (5)
Agosto de 2011 (2)
Julio de 2011 (2)
Junio de 2011 (4)
Mayo de 2011 (2)
Abril de 2011 (6)
Marzo de 2011 (4)
Febrero de 2011 (10)
Enero de 2011 (5)
Diciembre de 2010 (6)
Noviembre de 2010 (4)
Octubre de 2010 (8)
Septiembre de 2010 (4)
Agosto de 2010 (1)
Julio de 2010 (3)
Mayo de 2010 (5)
Abril de 2010 (6)
Marzo de 2010 (8)
Febrero de 2010 (3)
Enero de 2010 (1)
Diciembre de 2009 (9)
Noviembre de 2009 (14)
Octubre de 2009 (2)
Septiembre de 2009 (8)
Agosto de 2009 (2)
Julio de 2009 (10)
Junio de 2009 (9)
Mayo de 2009 (10)
Abril de 2009 (9)
Marzo de 2009 (3)
Febrero de 2009 (2)
Enero de 2009 (3)
Noviembre de 2008 (2)
Octubre de 2008 (2)
Septiembre de 2008 (2)
Agosto de 2008 (5)
Julio de 2008 (5)
Junio de 2008 (1)
Mayo de 2008 (3)
Abril de 2008 (2)
Marzo de 2008 (2)
Febrero de 2008 (2)
Enero de 2008 (5)
Noviembre de 2007 (2)
Octubre de 2007 (2)






Copyright © 2007 GuilleSQL, todos los derechos reservados.