Ir al contenido
wannacode

Inyección SQL y parámetros

En resumen: La inyección SQL es un ataque en el que la entrada del usuario pegada en una consulta se ejecuta como SQL, y una consulta parametrizada (prepared statement) lo impide.

Problema

El cuadro de búsqueda del panel de administración

El panel de administración tiene un cuadro «Correo del cliente». El programa toma lo que se escribió y lo pone en la consulta:

SELECT name, email, city FROM customers WHERE email = '<lo que se escribió>'

Con maria.ivanova@example.com obtienes a Maria. Pero lo que se escribe puede ser cualquier texto, también una comilla. Entonces deja de ser un valor y pasa a ser parte de la consulta. Esto se llama inyección SQL: el visitante escribe SQL en lugar del programa.

Pruébalo

Mostrar a todos los clientes

Pruébalo tú. El objetivo es lograr que la página muestre a todos los clientes sin conocer ni un solo correo. Mira la línea «Lo que llega a la base de datos»: ese texto exacto es lo que ejecuta la base.

La página busca a un cliente por su correo.base de datos: tienda

Lo que llega a la base de datos

Más precisamente, para curiosos

Una pista: una comilla cierra el texto y después necesitas una condición que sea verdadera para cada fila. Por ejemplo x' OR '1'='1. Dejas fuera la última comilla, porque el programa la añade solo.

Míralo

Dos trucos más

La entrada no tiene que ser una condición ingeniosa. El comentario -- elimina todo lo que va detrás, incluida una comprobación que el programa puso después del correo:

SQLbase de datos: tienda

El programa busca a un cliente con este correo de Varna, y sale Maria de Sofía: la comprobación de la ciudad terminó en el comentario.

Si el controlador ejecuta varias sentencias de un solo texto, la entrada x'; DELETE FROM customers; -- añade una sentencia nueva entera. Pruébalo en el cuadro de arriba (luego «Base nueva»). En el mundo real, una entrada así borra tablas, cambia contraseñas o lee datos que nunca debería ver.

La idea

Lo mismo en código: cuatro controladores, cuatro comportamientos

Aquí está la búsqueda con texto pegado en los cuatro lenguajes. El programa busca tres cosas: un correo real, x' OR '1'='1 y x'; DELETE FROM customers; --, y al final cuenta los clientes:

static void find(Connection conn, String email) throws SQLException {
String sql = "SELECT name FROM customers WHERE email = '" + email + "'";
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql)) {
List<String> names = new ArrayList<>();
while (rs.next()) {
names.add(rs.getString("name"));
}
System.out.println(email + " → " + names);
}
}
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
setup(conn);
find(conn, "maria.ivanova@example.com");
find(conn, "x' OR '1'='1");
try {
find(conn, "x'; DELETE FROM customers; --");
} catch (SQLException e) {
System.out.println("error: " + e.getMessage());
}
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM customers")) {
rs.next();
System.out.println("clientes en la base de datos: " + rs.getInt(1));
}
}
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
setup(conn);
find(conn, "maria.ivanova@example.com");
find(conn, "x' OR '1'='1");
try {
find(conn, "x'; DELETE FROM customers; --");
} catch (SQLException e) {
System.out.println("error: " + e.getMessage());
}
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM customers")) {
rs.next();
System.out.println("clientes en la base de datos: " + rs.getInt(1));
}
}
Salida
maria.ivanova@example.com → [Maria Ivanova]
x' OR '1'='1 → [Maria Ivanova, Georgi Petrov, Ivana Koleva, Dimitar Georgiev, Radost Hristova]
x'; DELETE FROM customers; -- → []
clientes en la base de datos: 5

Con OR '1'='1' los cuatro muestran a todos los clientes. Una segunda sentencia en el mismo texto es otra historia. En C# Microsoft.Data.Sqlite la ejecuta y los clientes bajan a 0. En Java executeQuery y en JavaScript prepare ejecutan solo la primera sentencia y no dicen nada del resto, mientras que Python se detiene con un error. El controlador no te protege: solo decide cuánto es el daño.

La idea

El parámetro

La consulta tiene un ? (en C# $email) donde va el valor, y el valor se envía por separado. La base de datos primero entiende la consulta, luego pone el valor, y el valor sigue siendo texto sea cual sea su contenido:

static void find(Connection conn, String email) throws SQLException {
String sql = "SELECT name FROM customers WHERE email = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, email);
try (ResultSet rs = ps.executeQuery()) {
List<String> names = new ArrayList<>();
while (rs.next()) {
names.add(rs.getString("name"));
}
System.out.println(email + " → " + names);
}
}
}
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
setup(conn);
find(conn, "maria.ivanova@example.com");
find(conn, "x' OR '1'='1");
try {
find(conn, "x'; DELETE FROM customers; --");
} catch (SQLException e) {
System.out.println("error: " + e.getMessage());
}
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM customers")) {
rs.next();
System.out.println("clientes en la base de datos: " + rs.getInt(1));
}
}
Salida
maria.ivanova@example.com → [Maria Ivanova]
x' OR '1'='1 → []
x'; DELETE FROM customers; -- → []
clientes en la base de datos: 5

Ahora x' OR '1'='1 es solo un correo que no existe, y el resultado está vacío. En el cuadro de arriba elige «Como parámetro» y ejecuta las mismas entradas: la consulta que llega a la base de datos ya no cambia.

Más precisamente, para curiosos

Todos los lenguajes y todos los controladores tienen parámetros: PreparedStatement en Java, Parameters en C#, el segundo argumento de execute en Python, los argumentos de all / get / run en node:sqlite. Los controladores de PostgreSQL usan $1, $2 (pg) o %s (psycopg). Un parámetro suele acelerar también las consultas repetidas, porque la base de datos puede reutilizar una consulta que ya entendió.

Un parámetro es para valores. Los nombres de tablas y columnas (por ejemplo, la columna de ORDER BY elegida en una lista desplegable) no pueden ser parámetros: ahí el programa compara la entrada con una lista de los permitidos (price, name) y no deja pasar nada más.

Ejercicio

Buscar por parte del correo

La búsqueda acepta también una parte de un correo. El programa envía la consulta del cuadro con el parámetro «petrov». Sustituye ? por el valor para ver qué devolverá la base de datos.

SQLbase de datos: tienda

Test

Ponte a prueba

1. El programa construye la consulta así: "... WHERE email = '" + email + "'". ¿Qué la hace segura?
2. ¿Por qué «x' OR '1'='1» devuelve a todos los clientes?
3. ¿Puede un parámetro sustituir el nombre de la columna en ORDER BY (ORDER BY ?)?
4. En Python, cursor.execute() con dos sentencias en el texto lanza un error. ¿Significa eso que la inyección no es peligrosa en Python?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·