Inyección SQL
Es momento de analizar la inyección SQL. Durante mucho tiempo, fue el rey indiscutible del OWASP Top 10, ocupando el primer puesto durante años consecutivos. A pesar de su antigüedad (más de 20 años) y de haber descendido ligeramente en esa lista, sigue siendo una vulnerabilidad increíblemente popular y peligrosa.
Como vulnerabilidad de seguridad web, la inyección SQL (SQLi) sigue siendo una de las técnicas de «hacking» más comunes utilizadas por los atacantes, ya que les permite manipular una base de datos y extraer información crucial de ella. Lo que es aún más alarmante es que un atacante puede llegar a convertirse en administrador del servidor de base de datos y realizar acciones devastadoras, como destruir bases de datos, manipular transacciones, divulgar datos y dejar el sistema expuesto a otros problemas.
Echemos un vistazo rápido a cómo sucede
SQL (o Structured Query Language) es el lenguaje utilizado para comunicarse con bases de datos relacionales; es el lenguaje de consulta que emplean desarrolladores, administradores de bases de datos y aplicaciones para gestionar la enorme cantidad de datos que se generan cada día.
Dentro de una aplicación, existen dos contextos: uno para los datos y otro para el código. El contexto del código le indica a las computadoras qué ejecutar y lo separa de los datos que deben procesarse. La inyección SQL ocurre cuando un atacante introduce datos que el intérprete SQL trata erróneamente como código, lo que le permite obtener información valiosa de la aplicación.
Efectos de un ataque de inyección SQL
Una inyección SQL puede ser extremadamente perjudicial para cualquier aplicación web y ha sido la técnica preferida en numerosas brechas de alto perfil porque proporciona a los atacantes acceso no autorizado a datos críticos. Pueden acceder a una gran cantidad de información, desde nombres de usuario y contraseñas hasta detalles de tarjetas de crédito y números de identificación personal.
Tras obtener acceso a estos datos, los atacantes pueden tomar el control de cuentas, restablecer contraseñas, realizar compras en línea o cometer otros tipos de fraude mucho más graves.
Pero quizás lo más alarmante de la SQLi es que un atacante puede, si no se detecta, mantener una puerta trasera en el sistema durante largos periodos de tiempo. Como puedes imaginar, esto conduciría a brechas de datos recurrentes mientras esa puerta trasera permanezca abierta. Es algo aterrador.
Veamos algunos ejemplos para entender mejor cómo funciona en la práctica.
Ejemplos de SQLi
La SQLi incluye diversas técnicas de vulnerabilidad que pueden abordar diferentes situaciones. A continuación, se presentan algunos de los ejemplos de SQLi más comunes:
Tipos de SQLi
Bien, ahora veamos los tres tipos diferentes de SQLi.
SQLi In-band
Este es uno de los tipos de inyección SQL más comunes, sencillos y eficientes. En este tipo de ataque, se utiliza el mismo canal de comunicación tanto para atacar como para recuperar los resultados.
A continuación, se presentan los dos tipos de ataques SQLi in-band:
- SQLi basada en UNION - El ataque basado en UNION utiliza el operador UNION para combinar dos o más consultas SQL, como las sentencias SELECT, para obtener la información deseada y los resultados en una respuesta HTTP GET.
- SQLi basada en errores - El atacante utiliza los mensajes de error de la base de datos para comprender su estructura. En este ataque, el atacante puede enviar solicitudes falsas o realizar acciones para que el servidor muestre mensajes de error y así obtener información de la base de datos. Por esta razón, es importante que los desarrolladores eviten enviar errores o mensajes de registro en el entorno de producción; en su lugar, deben almacenarse con acceso restringido.
SQLi inferencial
Los ataques de SQLi inferencial o "a ciegas" son más complejos y pueden requerir más tiempo para ser explotados. Además, el atacante no obtiene los resultados del ataque de forma inmediata, lo cual es precisamente lo que lo define como un ataque a ciegas.
El atacante envía cargas útiles (payloads) mediante solicitudes HTTP al servidor de base de datos para reestructurar la base de datos del usuario; luego, observa la respuesta y el comportamiento de la aplicación para determinar si el ataque tuvo éxito o no.
Existen dos tipos de ataques de SQLi inferencial:
- SQLi a ciegas basado en booleanos - En este ataque, se envía una consulta a la base de datos que devuelve un resultado booleano (verdadero o falso), y el atacante observa la respuesta HTTP para predecir dicho resultado.
- SQLi a ciegas basado en tiempo - En este ataque, el atacante envía una consulta a la base de datos para obligarla a esperar unos segundos antes de enviar la respuesta, y deduce los resultados de la consulta basándose en el tiempo de respuesta de la solicitud HTTP.
SQLi fuera de banda (Out-of-band)
Este es un tipo de ataque SQLi menos común que depende de las funciones habilitadas en el servidor de base de datos. Ocurre en casos donde el atacante no puede utilizar los otros tipos de ataque.
Por ejemplo, si no puede usar el mismo canal de comunicación para el ataque en banda, o si la respuesta HTTP no es lo suficientemente clara para determinar los resultados de la consulta.
Además, no es muy frecuente debido a su gran dependencia de la capacidad del servidor de base de datos para realizar solicitudes HTTP o DNS con el fin de enviar los datos requeridos al atacante.
Cómo defenderse contra SQLi
Afortunadamente, el lado positivo de que la inyección SQL sea tan antigua y común es que existen formas de prevenirla. Implementar este tipo de técnicas de prevención no solo es una buena práctica de programación, sino que reforzará significativamente la seguridad de una organización frente a SQLi.
Existen múltiples formas de proteger los servidores de bases de datos contra este tipo de ataques, como la validación de entradas, el uso de un firewall de aplicaciones web (WAF), el aseguramiento de las bases de datos, la contratación de equipos o sistemas de seguridad externos y la redacción de consultas SQL a prueba de errores.
Veamos un ejemplo de cómo prevenir inyecciones SQL en Python empleando una de las medidas de seguridad mencionadas anteriormente.
Ejemplo en Python
En este ejemplo, el atacante utilizará una inyección SQL a ciegas basada en booleanos para extraer información importante del sistema.
Python: Vulnerable
Supongamos que existe una tabla llamada "sample_data" en la base de datos. Esta tabla almacena los nombres de usuario y las contraseñas de los usuarios de la aplicación.
Ahora, permitamos que el usuario busque un valor en esta tabla de la base de datos mediante los siguientes comandos:
import mysql.connector
db = mysql.connector.connect
#Mala práctica. ¡Evítala! Esto es solo para fines educativos.
(host="localhost", user="newuser", passwd="pass", db="sample")
cur = db.cursor()
name = raw_input('Enter Name: ')
cur.execute("SELECT * FROM sample_data WHERE Name = '%s';" % name) for row in cur.fetchall(): print(row)
db.close()
Inyección SQL
Aquí, si el usuario introduce un nombre en la búsqueda, por ejemplo, Alicia, no habrá ningún problema con el resultado.
Sin embargo, si el usuario introduce algo como Alicia’; DROP TABLE sample_data; esto afectará significativamente a la base de datos.
Python: Corrección
La sentencia SQL debe cambiarse a la siguiente para evitar que ocurra el ataque:
cur.execute("SELECT * FROM sample_data WHERE Name = %s;", (name,))
Ahora, el sistema tratará la entrada del usuario como una cadena, incluso si el usuario intenta inyectar consultas SQL en ella, y tratará la entrada del usuario únicamente como el valor del nombre.
Este sencillo cambio puede prevenir actividades maliciosas en consultas futuras y proteger el sistema contra ataques basados en la entrada del usuario.
Ejemplo en Java
Para este ejemplo, también utilizaremos una tabla de base de datos llamada “sample_data” que almacena los datos de usuario de la aplicación.
Una página de inicio de sesión básica toma un nombre de usuario y una contraseña, y el archivo Java, que es un servlet (LoginServlet), los valida contra la base de datos para permitir la operación de inicio de sesión.
Java: Ejemplo vulnerable
Utilizando la tabla “sample_data” en la base de datos, el sistema permite a los usuarios realizar operaciones de inicio de sesión tomando sus credenciales como entrada.
Existe una consulta en el archivo LoginServlet para gestionar la operación de inicio de sesión, la cual es:
//Ejemplo incorrecto. No utilice concatenación de cadenas.
String query = "select * from sample_data where username='" + username + "' and password = '" + password + "'";
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
if (rs.next()) {
// Inicio de sesión exitoso si se encuentra una coincidencia
success = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {}
}
if (success) {
response.sendRedirect("home.html");
} else {
response.sendRedirect("login.html?error=1");
}
}
La siguiente es la consulta para el inicio de sesión del usuario:
select * from sample_data where username='username' and password ='password'
Inyección SQL
El sistema funcionará perfectamente si la entrada es válida. Por ejemplo, digamos que el nombre de usuario es Alicia nuevamente y la contraseña es secret.
El sistema devolverá los datos del usuario con esas credenciales. Sin embargo, un atacante puede manipular la solicitud del usuario utilizando Postman y cURL para realizar una inyección SQL.
Por ejemplo, el hacker puede enviar un nombre de usuario ficticio (Alicia) y la contraseña ‘or ‘1’=’1’.
En este caso, el nombre de usuario y la contraseña no coincidirán, pero la condición ‘1’=’1’ siempre será verdadera, por lo que la operación de inicio de sesión será exitosa.
Java: Prevención
Para la prevención, debemos modificar el código de LoginValidation y utilizar PreparedStatement en lugar de Statement para la ejecución de consultas. Este cambio evitará concatenar el nombre de usuario y la contraseña en la consulta, tratándolos como datos de configuración para evitar la inyección SQL.
A continuación se muestra el código modificado para LoginValidation:
String query = "select * from sample_data where username=? and password = ?";
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.prepareStatement(query);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
success = true;
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {
}
}
En este caso, PreparedStatement, los setters y la API JDBC subyacente se encargarán de la entrada del usuario y evitarán la inyección SQL.

Ejemplos
Ahora veremos algunos ejemplos más en varios lenguajes para entender mejor cómo funciona esto en la práctica.
C# - Inseguro
Este ejemplo es inseguro debido al uso de `FromRawSql`. Este método no vincula los parámetros ni intenta escaparlos. Por lo tanto, debe evitarse a toda costa.
var blogs = context.Posts
.FromRawSql("SELECT * FROM Posts WHERE state = {0} AND author = {1}", state, author)
.ToList();
C# - Seguro
Este ejemplo es seguro gracias a `FromSqlInterpolated`, que toma los valores interpolados y los parametriza.
Aunque generalmente es seguro, corre el riesgo de ser muy similar a `FromRawSql`, que no lo es.
var blogs = context.Posts
.FromSqlInterpolated($"SELECT * FROM Posts WHERE state = {state} AND author = {author}")
.ToList();
Java - Seguro: Hibernate - Consulta con nombre + Consulta nativa
Hibernate ofrece dos métodos para construir consultas de forma segura mediante sus funciones `Native Query` y `Named Query`. Ambas permiten especificar ubicaciones para los parámetros.
@NamedNativeQuery(
name = "find_post_by_state_and_author",
query =
"SELECT * " +
"FROM Post " +
"WHERE state = :state" +
" AND author = :author",
resultClass = Post.class)
java
List<Post> posts = session.createNativeQuery(
"SELECT * " +
"FROM Post " +
"WHERE state = :state" +
" AND author = :author" )
.addEntity(Post.class)
.setParameter("state", state)
.setParameter("author", author)
.list();
Java - Seguro: jplq
Al anotar un atributo `Query` en una interfaz de repositorio jplq, estos pueden adoptar múltiples formas y ser parametrizados.
@Query("SELECT p FROM Post p WHERE u.state = ?1 and u.author = ?2")
Post findPostByStateAndAuthor(String state, int author);
@Query("SELECT p FROM Post p WHERE u.state = :state and u.author = :author")
User findPostByStateAndAuthor(@Param("state") String state, @Param("author") int author);
Javascript - Seguro: pg
Al utilizar la biblioteca `pg`, el método `query` permite la parametrización proporcionando los valores de los parámetros a través de su segundo argumento.
const { posts } = await db.query('SELECT * FROM Post WHERE state = $1 AND author = $2', [state, author])
Javascript - Seguro: Sequelize
La biblioteca `sequelize` permite parametrizar consultas mediante su segundo argumento, el cual acepta la configuración de la consulta. Esto incluye una lista de valores para vincular a la consulta como parámetros, ya sea por nombre o por índice.
await sequelize.query(
'SELECT * FROM Post WHERE state = $state AND author = $author',
{
bind: { state: state, author: author},
type: QueryTypes.SELECT
}
);
await sequelize.query(
'SELECT * FROM Post WHERE state = $1 AND author = $2',
{
bind: [state, author],
type: QueryTypes.SELECT
}
);
Programación segura y práctica
Explore las directrices de programación segura para comprender y mitigar vulnerabilidades de software como el OWASP Top 10, y sumérjase en misiones de formación guiada para practicar en simulaciones de aplicaciones del mundo real.