arrow_backVolver a field notes
WEB SECURITY Publicado 8 ago 2026

¿Qué es SQL Injection y cómo se previene?

Un análisis práctico de ataques SQL injection, ejemplos reales y las soluciones concretas que los detienen en código de producción.

SQL injection existe desde finales de los años 90 y sigue siendo una de las formas más comunes en que se comprometen las aplicaciones web. El bug es simple: un atacante envía entrada que se interpreta como código SQL en lugar de datos simples, y la base de datos hace algo que nunca debería hacer.

Cómo funciona el ataque en realidad

Imagina un formulario de login que construye una consulta así en PHP:

$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";

Si la app no desinfecta la entrada, un atacante escribe admin' -- en el campo de usuario. La consulta se convierte en:

SELECT * FROM users WHERE username = 'admin' --' AND password = ''

El -- comenta el resto de la línea, así que la verificación de contraseña nunca ocurre. Ese es un bypass de autenticación clásico. Los atacantes también usan UNION SELECT para extraer datos de otras tablas, o apilan consultas con un punto y coma para ejecutar algo destructivo como DROP TABLE users; si el driver permite múltiples sentencias.

También existe SQL injection ciego, donde la app no muestra los resultados de la consulta directamente. Los atacantes deducen información a través de timing (SLEEP(5) en MySQL) o respuestas booleanas (¿se comporta la página diferente para una condición verdadera vs. falsa?). Herramientas como sqlmap automatizan este tipo de extracción una vez se encuentra un parámetro vulnerable.

Por qué la concatenación de strings es el problema raíz

Cada variante de este ataque se reduce a una cosa: mezclar código y datos en la misma string. La base de datos no puede distinguir entre un valor legítimo y sintaxis inyectada porque llegan por el mismo canal. Escapar comillas ayuda en algunos casos pero es frágil — distintas codificaciones, inyección de segundo orden (datos almacenados una vez, luego reutilizados inseguramente después), y peculiaridades específicas del driver crean caminos de bypass. Escapar es un parche, no una solución.

Consultas parametrizadas son la verdadera solución

La solución es separar código SQL de datos del usuario a nivel del driver, usando consultas parametrizadas (también llamadas prepared statements). La base de datos recibe la estructura de la consulta primero, luego vincula valores después, así que la entrada del usuario nunca puede cambiar el significado de la consulta.

En Python con psycopg2:

cur.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))

En Node.js con mysql2:

connection.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);

En Java con JDBC:

PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
stmt.setString(1, username);
stmt.setString(2, password);

Observa el patrón: placeholders (%s, ?) sostienen el lugar para datos, y los valores reales se pasan por separado. Sin concatenación de strings, sin escapado manual necesario. Esto funciona para la gran mayoría de consultas que escribirás en una aplicación normal.

¿Qué hay de nombres de tabla o columna dinámicos?

La parametrización maneja valores pero no identificadores — no puedes vincular un nombre de tabla como parámetro. Si tu app necesita seleccionar una tabla dinámicamente (raro, y frecuentemente una señal de mal diseño), usa whitelist de los valores permitidos contra una lista codificada en lugar de confiar directamente en entrada del usuario:

allowed_tables = {'orders', 'invoices', 'customers'}
if table_name not in allowed_tables:
    raise ValueError("Invalid table")

Nunca construyas nombres de identificadores a través de formateo de string desde entrada del usuario, incluso con escapado.

Defensas en capas más allá de la consulta misma

Las consultas parametrizadas son el control primario, pero algunas otras cosas importan:

  • Menor privilegio en la cuenta de base de datos. El usuario de DB de la app no debería tener DROP, ALTER, o acceso a esquemas no relacionados. Si una inyección se cuela, privilegios limitados cierran el daño.
  • Los ORMs ayudan por defecto. El ORM de Django, SQLAlchemy e Hibernate todos parametrizan consultas automáticamente cuando usas sus métodos estándar de construcción de consultas. El riesgo reaparece cuando desarrolladores caen en SQL crudo o usan llamadas .extra()/text() con interpolación de strings — así que audita esos lugares específicamente.
  • La validación de entrada es una capa secundaria, no un reemplazo. Verificar que un campo de email se vea como un email es buena práctica, pero no detiene inyección por sí sola — atacantes encuentran payloads creativos que aún pasan validación laxa.
  • Un WAF puede detectar patrones de ataque conocidos, pero es una capa de detección, no una solución para el código subyacente.

Prueba tu propio código para este bug

Pasa tus consultas a través de una herramienta de análisis estático (Bandit para Python, Semgrep con rulesets de SQL injection) como parte de CI. Para pruebas manuales, intenta inyectar una comilla simple (') en cada campo de entrada y observa si hay mensajes de error SQL filtrándose en la respuesta — a menudo es la primera señal de que una consulta no está parametrizada.

Si quieres profundizar en esto, el Web Security track de Korra Studio cubre inyección junto con XSS y auth bypass, y los segmentos de Databases te guían a través de patrones de diseño de consultas que evitan completamente esta clase de bug.

Escrito con asistencia de IA, revisado y publicado por Michal Pilch (CISSP), Korra Studio.

¿Listo para ir más allá?

Esta es una nota de la base de conocimiento de Korra Studio — la plataforma combina cada tema con mentoría 1 a 1.

Empezar gratisarrow_forward