SQL Injection es una de las vulnerabilidades web más peligrosas. Tanto es así que ha sido el elemento número 1 en el Top 10 de OWASP durante muchos años. Representa una amenaza seria porque la injección SQL permite la ejecución de código malicioso del atacante cambiando la estructura de la declaración SQL de una aplicación web de una manera que puede robar datos, modificar datos, o potencialmente facilitar la inyección de comandos al sistema operativo subyacente. Debemos tener en cuenta además que las sentencias SQL se ejecutan con rol de administrador con lo cual las consecuencias pueden llegar a ser desastrosas.
Nociones breves acerca de SQL Injection
¿Qué son los ataques SQL Injection? Según el proyecto OWASP: ”Un ataque por inyección SQL consiste en la inserción o “inyección” de una consulta SQL por medio de los datos de entrada desde el cliente hacia la aplicación. Un ataque por inyección SQL exitoso puede leer información sensible desde la base de datos, modificar la información (Insert/ Update/ Delete), ejecutar operaciones de administración sobre la base de datos (tal como parar la base de datos), recuperar el contenido de un determinado archivo presente sobre el sistema de archivos del DBMS y en algunos casos emitir comandos al sistema operativo. Los ataques por inyección SQL son un tipo de ataque de inyección, en el cual los comandos SQL son insertados en la entrada de datos con la finalidad de efectuar la ejecución de comandos SQL predefinidos.”
Es decir, permite insertar sentencias sql, con devastadoras consecuencias si se desea, allí donde en principio no se debiera. De hecho siempre está en el Top Ten del proyecto desde hace muchos años.
¿Esto por qué sucede? Porque la mayoría de las aplicaciones web desarrolladas hoy en día hacen uso de una base de datos para ofrecer páginas dinámicas y almacenar información, tanto de los usuarios como de la propia herramienta, datos a los que se accede por medio del lenguaje SQL, un lenguaje para interaccionar con bases de datos relacionales.
El uso de este tipo de lenguaje ha traído consigo la aparición de una de las vulnerabilidades más peligrosas a la que nos podemos enfrentar, una vulnerabilidad que no solamente pone en riesgo la integridad de la aplicación, sino de todos los datos almacenados de los usuarios que la utilicen, y que se produce cuando no se filtra de forma correcta la información enviada por los usuario.
¿Cómo funciona?
Mediante la inserción de código SQL por medio de los datos de entrada, desde la parte del cliente, hacia la aplicación. Es decir, por medio de la inserción de este código, el atacante puede modificar las consultas originales que debe realizar la aplicación y hacerla ejecutar otras totalmente distintas con la intención de acceder a las bases de datos, obtener información de alguna de las tablas o borrar los datos almacenados, entre otras muchas cosas. Como consecuencia directa de estos ataques y dependiendo de los privilegios que tenga el usuario de la base de datos bajo el que se ejecutan las consultas, se puede acceder no sólo a las tablas relacionadas con la aplicación, sino también a otras tablas, pertenecientes a otras bases de datos alojadas en ese mismo servidor, con lo que el posible impacto se puede magnificar.
Todo lo comentado en las líneas anteriores, es posible gracias al uso de ciertos caracteres en los campos de entrada de información por parte del usuario, ya sea mediante el uso de los campos de los formularios que son enviados al servidor mediante POST o bien por medio de los datos enviados mediante GET en las urls de las páginas web que posibilitan coordinar varias consultas SQL o ignorar el resto de la consulta, permitiendo al usuario malicioso ejecutar la consulta que elija, de ahí que sea de obligado cumplimiento, realizar un filtrado de esos datos enviados para evitar problemas en el futuro.

El atacante intentará provocar errores en la aplicación para, mediante la revisión de los logs de esos errores, extraer información que luego usará para insertar sus sentencias.
Cuando una aplicación controla los mensajes de error y no permite la impresión de ningún tipo de mensaje, se puede realizar otro tipo de ataque, el ataque “Blind SQL Injection”, traducido al español como “Ataque a ciegas por inyección de SQL”, que se da cuando en una página web no aparece ningún mensaje de error al ejecutar una sentencia SQL errónea, por lo que el atacante va realizando pruebas hasta dar con el nombre de los campos o tablas sobre los que poder actuar. Entre las bases de datos susceptibles a este tipo de ataques nos encontramos MySQL, Oracle, Postgres o MS SQL y MariaDB.
¿Qué problemas pueden causar este tipo de ataques?
Integridad: Al igual que un ataque por inyección SQL permite leer información relevante almacenada en la base de datos, también es posible realizar cambios o incluso borrar toda información mediante este tipo de vulnerabilidad. Se han dado casos de webs en las se han dumpeado las bases de datos y posteriormente, se han eliminado las que había, luego se han pedido rescates.
Confidencialidad: ¿Para qué se usan las BBDD? Las bases de datos almacenan información sensible, por lo que la pérdida de confiabilidad es un problema muy frecuente en aquellos sitios que son vulnerables a este tipo de ataques. Nadie vuelve a confiar en una plataforma a la que le han robado sus bases de datos.
Identificación: Si el sistema de logueo que se utiliza para acceder a una zona restringida de una web es débil, por medio de este tipo de ataques se podría acceder sin la necesidad de conocer ni el usuario ni la contraseña, en nuestro entorno de trabajo esto podría ser catastrófico en según qué circunstancias.
Identificación 2: Se pueden crear Usuarios que no existían anteriormente, con derechos de administrador y no conocidos. Podrían entrar quien quisiera, desde donde quisiera y hacer lo que desearan.
Ejemplo de explotación:

Éste es un típico formulario donde se pide un nombre de usuario y una contraseña para acceder a una zona privada. El código del lado del servidor que forma la consulta sería algo como lo siguiente:
$username = $_POST[‘username’];
- $password = $_POST[‘password’];
- $sql = » SELECT * FROM usuarios WHERE username = ‘$username’ AND password = ‘$password’ «;
Como vemos, primero se almacenan el nombre de usuario y contraseña ingresadas, en las variables username y password respectivamente, seguidamente se incluyen estos valores en la consulta que será enviada a la base de datos para comprobar si existe dicho usuario. Supongamos en este caso que el usuario ingresa como nombre de usuario admin y contraseña pass123, al enviarse el formulario, la consulta formada será la siguiente:
- SELECT * FROM usuarios WHERE username = ‘admin’ AND password = ‘pass123’
Como se ve, al colocarse los datos de entrada, éstos quedan entre las comillas simples para representar que se trata de una cadena de texto. Esta consulta será enviada a la base de datos y nos devolverá un resultado con los datos de dicho usuario, si existe, y se permitirá el acceso a la zona privada, de lo contrario devolverá un resultado vacío y se impedirá negando el acceso.
Como se mencionó anteriormente, SQL injection consiste en anexar código SQL a una consulta, y este formulario lo permite mediante los campos de entrada, de forma que tenemos una aplicación vulnerable a SQL injection.
¿Cómo se explota?
El objeto de explotar esta vulnerabilidad es conseguir acceso a la zona privada sin conocer ni el usuario, ni contraseña correcta, y aprovechar la vulnerabilidad. De forma que lo que tenemos que lograr es inyectar código SQL para formar una consulta que retorne un resultado válido.
Veamos cómo queda formada la consulta si inyectamos el siguiente código SQL en el campo contraseña:

Al formarse la consulta, quedará de la siguiente manera:
- SELECT * FROM usuarios WHERE username = ‘hacker’ AND password = » or 1=1#’
Hay que prestar atención en que el código insertado, se encuentra entre las comillas simples que encierran el password. La comilla simple al inicio del código insertado se encarga de completar la comilla abierta en la parte de la consulta password = ‘ , de esta forma obtenemos temporalmente la siguiente consulta:
- SELECT * FROM usuarios WHERE username = ‘hacker’ AND password = »
Esta consulta de momento, no devolverá resultados pues no existe tal usuario con esas credenciales, sin embargo vamos a analizar el resto del código insertado:
- or 1=1#
La sentencia or 1=1 cambia radicalmente la lógica de la consulta, ya que como sabemos en una consulta formada por el condicional OR devolverá verdadero al cumplirse al menos una de las dos expresiones, en nuestro caso la primera expresión es username = ‘hacker’ AND password = » , y la segunda or 1=1 , esta última se cumple siempre, es decir 1 es siempre igual a 1, por lo que la consulta nos devolverá un resultado válido.
Por último tan solo queda deshacernos de la comilla que cierra la sentencia, para esto podemos usar los comentarios utilizados en SQL: # , — (doble guión), o bien /* */ . De esta forma la consulta completa es:
- SELECT * FROM usuarios WHERE username = ‘hacker’ AND password = » or 1=1#’
Todo después del # será tomado en cuenta como comentario y no formará parte de la consulta.
Para conseguir obtener un resultado válido existen muchas otras variaciones en el código que podemos insertar, por ejemplo algunas de ellas…

¿Cómo paliar o anular esta amenaza?
A la hora de desarrollar una aplicación, es muy complicado crear una herramienta totalmente segura a las primeras de cambio y si encima se “hereda” de otros desarrolladores anteriores aún más, a esto puede añadirse versiones viejas de software, framework usados y ya sin soporte, etc. La falta de tiempo con las consiguientes presiones y la intervención de varios programadores para su desarrollo, son factores que también juegan en contra de la seguridad. A pesar de estos inconvenientes, siempre se pueden tomar medidas de seguridad que nos ayuden a desarrollar aplicaciones más robustas, ajenas en la medida de lo posible, a este tipo de problemas.
Vamos a ver algunos consejos para evitar sufrir el ataque por inyección de código SQL en nuestros desarrollos o aplicaciones en producción:
1. Escapar los caracteres especiales utilizados en las consultas SQL
Al hablar de “escapar caracteres” estamos haciendo referencia a añadir la barra invertida “\” delante de las cadenas utilizadas en las consultas SQL para evitar que éstas corrompan la consulta. Algunos de estos caracteres especiales que es aconsejable escapar son las comillas dobles (“), las comillas simples (‘) o los caracteres \x00 o \x1a ya que son considerados como peligrosos pues pueden ser utilizados durante los ataques.
Los distintos lenguajes de programación ofrecen mecanismos para lograr escapar estos caracteres. En el caso de PHP podemos optar por la función mysql_real_scape_string(), que toma como parámetro una cadena y la modifica evitando todos los caracteres especiales, devolviéndola totalmente segura para ser ejecutada dentro de la instrucción SQL. En Java la clase StringEscapeUtils, de la librería Lang del proyecto Apache Commons puede ayudar bastante, en según que circunstancias a escapar de según qué caracteres evitando con ello ataques de Inyección de SQL o XSS Cross-Site Scripting por poner varios ejemplos.
2. Delimitar los valores de las consultas
Aunque el valor de la consulta sea un entero, es aconsejable delimitarlo siempre entre comillas simples. Una instrucción SQL del tipo:
SELECT nombre FROM usuarios WHERE id_user = $id
Será mucho más fácilmente inyectable que:
SELECT nombre FROM usuarios WHERE id_user = ‘$id’
Donde $id es un número entero.
3. Verificar siempre los datos que introduce el usuario
Si en una consulta estamos a la espera de recibir un entero, no confiemos en que sea así, sino que es aconsejable tomar medidas de seguridad y realizar la comprobación de que realmente se trata del tipo de dato que estamos esperando. Para realizar esto, los lenguajes de programación ofrecen funciones que realizan esta acción, como pueden ser ctype_digit() para saber si es un número o ctype_alpha () para saber si se trata de una cadena de texto en el caso del lenguaje PHP.
También es aconsejable comprobar la longitud de los datos para descartar posibles técnicas de inyección SQL, ya que si por ejemplo estamos esperando un nombre, una cadena extremadamente larga puede suponer que estén intentando atacarnos por este método. En el caso de PHP, podemos utilizar la función strlen() para ver el tamaño de la cadena.
4. Asignar mínimos privilegios al usuario que conectará con la base de datos
El usuario que utilicemos para conectarnos a la base de datos desde nuestro código debe tener los privilegios justos para realizar las acciones que necesitemos. No utilizar nunca un usuario root con acceso a todas las bases de datos ya que de esta forma estaremos dando facilidades a los hackers para que puedan acceder a toda la información.
5. Programar bien
Aunque pueda parecer una tontería, no hay mejor medida para evitar este tipo de ataques que realizar una buena programación, poniendo en práctica las necesidades básicas y el interés para desarrollar una aplicación totalmente segura.
6. Uso de sentencias preparadas parametrizadas
Por ejemplo usando la librería ESAPI, (sin ser la panacea) puede llegar a ser muy útil.
SQL VULNERABLE
// Validación de datos de entrada con ESAPI. userId= ESAPI.encoder().encodeForSQL(new MySQLCodec(MySQLCodec.Mode.STANDARD), request.getParameter(«userId «)); String sqlString=”SELECT * FROM users WHERE userId = ’”+ form.getUserId()+”’”; SQL SEGURA. // Validación de datos de entrada con ESAPI. userId= ESAPI.encoder().encodeForSQL(new MySQLCodec(MySQLCodec.Mode.STANDARD), request.getParameter(«userId «)); // Consulta Preparada. String consulta = «SELECT * FROM users WHERE userId = ?;”; PreparedStatement pstmt = connection.prepareStatement (consulta); pstmt.setString( 1, custname);pstmt.setString (1, userId); ResultSet results = pstmt.executeQuery( ); |
Ejemplos en varios lenguajes
Language – Library | Parameterized Query |
Java – Standard | String custname = request.getParameter(«customerName»);
String query = «SELECT account_balance FROM user_data WHERE user_name = ? «; PreparedStatement pstmt = connection.prepareStatement( query ); pstmt.setString( 1, custname); ResultSet results = pstmt.executeQuery( ); |
Java – Hibernate | //HQL
@Entity // declare as entity; @NamedQuery( name=»findByDescription», query=»FROM Inventory i WHERE i.productDescription = :productDescription» ) public class Inventory implements Serializable { @Id private long id; private String productDescription; } // use case String userSuppliedParameter = request.getParameter(«Product-Description»); // This should REALLY be validated too // perform input validation to detect attacks List<Inventory> list = session.getNamedQuery(«findByDescription») .setParameter(«productDescription», userSuppliedParameter).list(); //Criteria API String userSuppliedParameter = request.getParameter(«Product-Description»); // This should REALLY be validated too // perform input validation to detect attacks Inventory inv = (Inventory) session.createCriteria(Inventory.class).add (Restrictions.eq(«productDescription», userSuppliedParameter)).uniqueResult(); |
.NET/C# | String query = «SELECT account_balance FROM user_data WHERE user_name = ?»;
try { OleDbCommand command = new OleDbCommand(query, connection); command.Parameters.Add(new OleDbParameter(«customerName», CustomerName Name.Text)); OleDbDataReader reader = command.ExecuteReader(); // … } catch (OleDbException se) { // error handling } |
ASP.NET | string sql = «SELECT * FROM Customers WHERE CustomerId = @CustomerId»;
SqlCommand command = new SqlCommand(sql); command.Parameters.Add(new SqlParameter(«@CustomerId», System.Data.SqlDbType.Int)); command.Parameters[«@CustomerId»].Value = 1; |
Ruby – ActiveRecord | # Create
Project.create!(:name => ‘owasp’) # Read Project.all(:conditions => «name = ?», name) Project.all(:conditions => { :name => name }) Project.where(«name = :name», :name => name) # Update project.update_attributes(:name => ‘owasp’) # Delete Project.delete(:name => ‘name’) |
Ruby | insert_new_user = db.prepare «INSERT INTO users (name, age, gender) VALUES (?, ? ,?)»
insert_new_user.execute ‘aizatto’, ’20’, ‘male’ |
PHP – PDO | $stmt = $dbh->prepare(«INSERT INTO REGISTRY (name, value) VALUES (:name, :value)»);
$stmt->bindParam(‘:name’, $name); $stmt->bindParam(‘:value’, $value); |
Cold Fusion | <cfquery name = «getFirst» dataSource = «cfsnippets»>
SELECT * FROM #strDatabasePrefix#_courses WHERE intCourseID = <cfqueryparam value = #intCourseID# CFSQLType = «CF_SQL_INTEGER»> </cfquery> |
Perl – DBI | my $sql = «INSERT INTO foo (bar, baz) VALUES ( ?, ? )»;
my $sth = $dbh->prepare( $sql ); $sth->execute( $bar, $baz ); |
7. Uso de procedimientos almacenados.
Los procedimientos almacenados no incluyen ninguna generación dinámica de SQL ya que se encuentran almacenados en Base de Datos. Usemos en este ejemplo, la anteriormente mencionada ESAPI
// Validación de datos de entrada con ESAPI
String idCliente = ESAPI.encoder().encodeForSQL(new MySQLCodec(MySQLCodec.Mode.STANDARD), request.getParameter(«idCliente»)); try { CallableStatement cs = connection.prepareCall(«{call sp_getUsuario(?)}»); cs.setString(1, idCliente); ResultSet results = cs.executeQuery(); // … resultado } catch (SQLException se) { // …tratar error. } |
Ejemplos en varios lenguajes
Language – Library | Parameterized Query |
Oracle – PL/SQL | Normal Stored Procedure – no dynamic SQL being created. Parameters passed in to stored procedures are naturally bound to their location within the query without anything special being required.
PROCEDURE SafeGetBalanceQuery( UserID varchar, Dept varchar) AS BEGIN SELECT balance FROM accounts_table WHERE user_ID = UserID AND department = Dept; END; |
Oracle – PL/SQL | Stored Procedure Using Bind Variables in SQL Run with EXECUTE. Bind variables are used to tell the database that the inputs to this dynamic SQL are ‘data’ and not possibly code.
PROCEDURE AnotherSafeGetBalanceQuery( UserID varchar, Dept varchar) AS stmt VARCHAR(400); result NUMBER; BEGIN stmt := ‘SELECT balance FROM accounts_table WHERE user_ID = :1 AND department = :2′; EXECUTE IMMEDIATE stmt INTO result USING UserID, Dept; RETURN result; END; |
SQL Server- Transact-SQL | Normal Stored Procedure – no dynamic SQL being created. Parameters passed in to stored procedures are naturally bound to their location within the query without anything special being required.
PROCEDURE SafeGetBalanceQuery( @UserID varchar(20), @Dept varchar(10)) AS BEGIN SELECT balance FROM accounts_table WHERE user_ID = @UserID AND department = @Dept END |
SQL Server- Transact-SQL | Stored Procedure Using Bind Variables in SQL Run with EXEC. Bind variables are used to tell the database that the inputs to this dynamic SQL are ‘data’ and not possibly code.
PROCEDURE SafeGetBalanceQuery(@UserID varchar(20), @Dept varchar(10)) AS BEGIN DECLARE @sql VARCHAR(200) SELECT @sql = ‘SELECT balance FROM accounts_table WHERE ‘ + ‘user_ID = @UID AND department = @DPT’ EXEC sp_executesql @sql, ‘@UID VARCHAR(20), @DPT VARCHAR(10)’, @UID=@UserID, @DPT=@Dept END |