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.
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:
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)); }}using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);Find(conn, "maria.ivanova@example.com");Find(conn, "x' OR '1'='1");try{ Find(conn, "x'; DELETE FROM customers; --");}catch (SqliteException e){ Console.WriteLine($"error: {e.Message}");}var count = conn.CreateCommand();count.CommandText = "SELECT COUNT(*) FROM customers";Console.WriteLine($"clientes en la base de datos: {count.ExecuteScalar()}");
static void Find(SqliteConnection conn, string email){ var cmd = conn.CreateCommand(); cmd.CommandText = $"SELECT name FROM customers WHERE email = '{email}'"; using var reader = cmd.ExecuteReader(); var names = new List<string>(); while (reader.Read()) { names.Add(reader.GetString(0)); } Console.WriteLine($"{email} → [{string.Join(", ", names)}]");}def find(conn, email): sql = f"SELECT name FROM customers WHERE email = '{email}'" names = [name for (name,) in conn.execute(sql)] print(f"{email} → [{', '.join(names)}]")
conn = sqlite3.connect(":memory:")setup(conn)find(conn, "maria.ivanova@example.com")find(conn, "x' OR '1'='1")try: find(conn, "x'; DELETE FROM customers; --")except sqlite3.Error as e: print("error:", e)print("clientes en la base de datos:", conn.execute("SELECT COUNT(*) FROM customers").fetchone()[0])conn.close()function find(db, email) { const sql = `SELECT name FROM customers WHERE email = '${email}'`; const names = db.prepare(sql).all().map((row) => row.name); console.log(`${email} → [${names.join(", ")}]`);}
const db = new DatabaseSync(":memory:");setup(db);find(db, "maria.ivanova@example.com");find(db, "x' OR '1'='1");try { find(db, "x'; DELETE FROM customers; --");} catch (e) { console.log("error:", e.message);}console.log("clientes en la base de datos:", db.prepare("SELECT COUNT(*) AS n FROM customers").get().n);db.close();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)); }}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: 5using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);Find(conn, "maria.ivanova@example.com");Find(conn, "x' OR '1'='1");try{ Find(conn, "x'; DELETE FROM customers; --");}catch (SqliteException e){ Console.WriteLine($"error: {e.Message}");}var count = conn.CreateCommand();count.CommandText = "SELECT COUNT(*) FROM customers";Console.WriteLine($"clientes en la base de datos: {count.ExecuteScalar()}");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: 0conn = sqlite3.connect(":memory:")setup(conn)find(conn, "maria.ivanova@example.com")find(conn, "x' OR '1'='1")try: find(conn, "x'; DELETE FROM customers; --")except sqlite3.Error as e: print("error:", e)print("clientes en la base de datos:", conn.execute("SELECT COUNT(*) FROM customers").fetchone()[0])conn.close()maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → [Maria Ivanova, Georgi Petrov, Ivana Koleva, Dimitar Georgiev, Radost Hristova]error: You can only execute one statement at a time.clientes en la base de datos: 5const db = new DatabaseSync(":memory:");setup(db);find(db, "maria.ivanova@example.com");find(db, "x' OR '1'='1");try { find(db, "x'; DELETE FROM customers; --");} catch (e) { console.log("error:", e.message);}console.log("clientes en la base de datos:", db.prepare("SELECT COUNT(*) AS n FROM customers").get().n);db.close();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: 5Con 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)); }}maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []clientes en la base de datos: 5using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);Find(conn, "maria.ivanova@example.com");Find(conn, "x' OR '1'='1");try{ Find(conn, "x'; DELETE FROM customers; --");}catch (SqliteException e){ Console.WriteLine($"error: {e.Message}");}var count = conn.CreateCommand();count.CommandText = "SELECT COUNT(*) FROM customers";Console.WriteLine($"clientes en la base de datos: {count.ExecuteScalar()}");
static void Find(SqliteConnection conn, string email){ var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT name FROM customers WHERE email = $email"; cmd.Parameters.AddWithValue("$email", email); using var reader = cmd.ExecuteReader(); var names = new List<string>(); while (reader.Read()) { names.Add(reader.GetString(0)); } Console.WriteLine($"{email} → [{string.Join(", ", names)}]");}maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []clientes en la base de datos: 5def find(conn, email): sql = "SELECT name FROM customers WHERE email = ?" names = [name for (name,) in conn.execute(sql, (email,))] print(f"{email} → [{', '.join(names)}]")
conn = sqlite3.connect(":memory:")setup(conn)find(conn, "maria.ivanova@example.com")find(conn, "x' OR '1'='1")try: find(conn, "x'; DELETE FROM customers; --")except sqlite3.Error as e: print("error:", e)print("clientes en la base de datos:", conn.execute("SELECT COUNT(*) FROM customers").fetchone()[0])conn.close()maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []clientes en la base de datos: 5function find(db, email) { const sql = "SELECT name FROM customers WHERE email = ?"; const names = db.prepare(sql).all(email).map((row) => row.name); console.log(`${email} → [${names.join(", ")}]`);}
const db = new DatabaseSync(":memory:");setup(db);find(db, "maria.ivanova@example.com");find(db, "x' OR '1'='1");try { find(db, "x'; DELETE FROM customers; --");} catch (e) { console.log("error:", e.message);}console.log("clientes en la base de datos:", db.prepare("SELECT COUNT(*) AS n FROM customers").get().n);db.close();maria.ivanova@example.com → [Maria Ivanova]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []clientes en la base de datos: 5Ahora 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.
Solución
SELECT name, email FROM customers WHERE email LIKE '%' || 'petrov' || '%';El % de antes y de después se une al valor en la propia consulta ('%' || ? || '%'), no en la entrada. Así el parámetro sigue siendo texto simple y LIKE recibe %petrov%.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: IN, BETWEEN y LIKE · Funciones de texto · DELETE: borrar filas · Conectarse a la base de datos desde el código
- Continúa en: ORM y el problema N+1 · Pools de conexiones, copias de seguridad y permisos
- Ejercicios sobre el tema: La base de datos en el programa