Saltar al contenido
Lección 2 de 8

Inyección e Inyección SQL

8 min read

Qué es un Ataque de Inyección

La inyección es una de las clases de vulnerabilidad más antiguas y peligrosas, y figura de forma destacada en el OWASP Top 10 (categoría A03:2021 — Injection). Ocurre cuando una aplicación toma datos no confiables proporcionados por el usuario y los inserta dentro de un comando o consulta que luego se ejecuta. El intérprete (la base de datos, el shell del sistema operativo, el motor de plantillas) no puede distinguir entre el código legítimo que escribió el desarrollador y los datos maliciosos que envió el atacante.

La raíz del problema es la mezcla de código y datos. Un intérprete recibe una sola cadena y debe decidir qué parte es instrucción y qué parte es valor. Cuando el desarrollador arma esa cadena pegando la entrada del usuario en medio de la instrucción, le entrega al atacante el poder de reescribir la gramática de la consulta. La entrada deja de ser un simple valor y pasa a formar parte de la estructura ejecutable. Todo lo demás —saltarse el login, leer tablas ajenas, borrar registros— es una consecuencia de esa confusión inicial entre lo que es código y lo que es dato.

La inyección SQL es la variante más conocida, pero la misma causa raíz aparece en muchas familias: inyección de comandos del sistema operativo (OS command injection), inyección LDAP, inyección de plantillas del lado del servidor (SSTI) e incluso inyección NoSQL en bases de datos orientadas a documentos. Todas comparten la misma defensa de fondo: separar de forma tajante el código de los datos, para que la entrada del usuario nunca pueda cambiar la estructura de lo que se ejecuta.

Cómo Funciona la Inyección SQL

La inyección SQL (CWE-89) es la variante más conocida. Imagina un formulario de login donde la aplicación construye una consulta concatenando el nombre de usuario y la contraseña que envió el usuario. Si esos valores se insertan tal cual en la cadena SQL, un atacante puede introducir caracteres especiales (como una comilla) para cerrar prematuramente un literal de texto y luego añadir sus propias condiciones SQL.

De forma conceptual, el atacante manipula la consulta para que la cláusula de comparación siempre sea verdadera, logrando así saltarse la autenticación sin conocer una contraseña válida. Variantes más avanzadas permiten usar operadores como UNION para combinar los resultados de la consulta original con datos de otras tablas, extrayendo así información que nunca debió ser accesible. No incluimos cargas útiles completas aquí porque el objetivo es comprender el mecanismo, no facilitar el abuso.

Tipos de Inyección SQL

No todas las inyecciones SQL devuelven los datos directamente en la página. Conviene conocer las familias principales. La inyección in-band es la más directa: los resultados aparecen en la misma respuesta, por ejemplo a través de un error de base de datos detallado o de una consulta UNION. Es la más fácil de detectar y explotar.

La inyección ciega (blind) ocurre cuando la aplicación no muestra los datos ni los errores, pero su comportamiento cambia según si una condición inyectada es verdadera o falsa. El atacante deduce la información observando diferencias en las respuestas. La variante basada en tiempo (time-based) introduce retardos artificiales en la base de datos para inferir datos bit a bit según cuánto tarda en responder. Por último, la inyección out-of-band exfiltra datos a través de un canal distinto, como una petición DNS o HTTP saliente, y resulta útil cuando el canal de respuesta directo está cerrado.

El Impacto Real

El impacto de una inyección SQL puede ser catastrófico. En el peor caso, un atacante puede leer toda la base de datos: credenciales, datos personales, información de tarjetas, secretos de negocio. Muchas de las mayores brechas de datos de la historia comenzaron con una simple inyección SQL en un parámetro olvidado: un campo de búsqueda, un filtro de ordenamiento o un identificador en la URL que nadie pensó en parametrizar.

Más allá de la lectura, según los permisos de la cuenta de base de datos, un atacante podría modificar o borrar registros, alterar precios, crear cuentas de administrador o incluso, en algunas configuraciones, ejecutar comandos en el sistema operativo del servidor. Por eso la inyección se considera de criticidad alta: combina facilidad de explotación con un impacto potencialmente total sobre la confidencialidad, integridad y disponibilidad de los datos.

Código Vulnerable y su Corrección

La mejor forma de interiorizar la defensa es ver el patrón inseguro junto a su versión segura. En todos los ejemplos, el problema es el mismo: la entrada del usuario se pega dentro de la cadena SQL. La solución también es la misma: enviar la consulta y los datos por separado, mediante marcadores de posición.

Node.js

En el patrón vulnerable, la variable del usuario se concatena directamente en el texto de la consulta. El motor recibe una sola cadena y no puede saber dónde termina la instrucción y dónde empieza el dato:

// VULNERABLE: concatenación de strings
import { pool } from "./db.js";

async function findUser(email) {
  // El valor de `email` se convierte en parte del código SQL.
  const sql = "SELECT id, name FROM users WHERE email = '" + email + "'";
  const { rows } = await pool.query(sql);
  return rows;
}

La versión segura usa una consulta parametrizada con marcadores de posición ($1 en pg, ? en mysql2). La estructura del SQL queda fija y el valor viaja aparte, tratado siempre como dato puro:

// SEGURO: consulta parametrizada
import { pool } from "./db.js";

async function findUser(email) {
  const sql = "SELECT id, name FROM users WHERE email = $1";
  const { rows } = await pool.query(sql, [email]);
  return rows;
}

Python

El mismo error aparece cuando se arma la consulta con un f-string o con el operador %. Aunque el código luzca limpio, la entrada del usuario termina dentro de la instrucción:

# VULNERABLE: f-string dentro de la consulta
def find_user(cursor, email):
    # `email` se interpola en el texto SQL antes de ejecutarse.
    cursor.execute(f"SELECT id, name FROM users WHERE email = '{email}'")
    return cursor.fetchall()

La versión segura pasa los valores como segundo argumento de execute. El driver de la base de datos aplica el marcador de posición (%s en la mayoría de drivers, ? en sqlite3) y vincula el dato de forma segura:

# SEGURO: parámetros vinculados
def find_user(cursor, email):
    cursor.execute("SELECT id, name FROM users WHERE email = %s", (email,))
    return cursor.fetchall()

Un ORM como SQLAlchemy o el ORM de Django parametriza por defecto: cuando escribes User.query.filter_by(email=email) o User.objects.filter(email=email), la biblioteca genera consultas vinculadas por debajo. Solo pierdes esa protección si vuelves a SQL crudo con interpolación manual, así que evita construir cadenas SQL a mano incluso dentro de un ORM.

Cómo Prevenirla

La defensa principal y definitiva contra la inyección SQL son las consultas parametrizadas (también llamadas prepared statements o sentencias preparadas). Con esta técnica, la estructura de la consulta SQL se define por separado de los datos. Los valores del usuario se envían como parámetros vinculados, y el motor de base de datos los trata siempre como datos puros, nunca como código ejecutable. Esto elimina la causa raíz: ya no hay forma de que la entrada del usuario cambie la estructura de la consulta.

Como capas adicionales de defensa en profundidad:

  • Mínimo privilegio en la cuenta de base de datos. La aplicación no debería conectarse como superusuario. Concede solo los permisos necesarios (por ejemplo, sin DROP ni acceso a tablas de sistema) para que, aunque haya una inyección, el daño sea limitado.
  • ORMs y capas de acceso bien construidas. Adopta herramientas que parametrizan por defecto y reserva el SQL crudo para casos justificados, siempre con parámetros.
  • Validación con listas de permitidos (allow-list). Algunos fragmentos no se pueden parametrizar, como el nombre de una columna en un ORDER BY dinámico. En esos casos, valida la entrada contra un conjunto cerrado de valores permitidos en lugar de intentar limpiarla.
  • Errores genéricos en producción. Desactiva los mensajes de error detallados de la base de datos de cara al usuario; delatan la estructura interna y facilitan la inyección ciega. Registra el detalle solo del lado del servidor.
  • No confíes en escapar caracteres a mano. Escapar manualmente es frágil: es fácil olvidar un caso, y las reglas varían entre motores y codificaciones. La parametrización es robusta porque el motor nunca reinterpreta el dato como código.

Para detectar estos fallos antes de que lleguen a producción, apóyate en herramientas legítimas de testing autorizado: Burp Suite y OWASP ZAP como proxies de análisis, escáneres SAST (análisis estático del código) y DAST (análisis dinámico de la aplicación en ejecución), y sqlmap como herramienta de verificación en entornos de prueba sobre sistemas propios o con permiso explícito. Estas herramientas confirman la defensa; no reemplazan escribir consultas parametrizadas desde el principio.

Checklist de Prevención

  • Usa siempre consultas parametrizadas / prepared statements; nunca concatenes entrada de usuario en el SQL.
  • Prefiere un ORM que parametrice por defecto y evita el SQL crudo con interpolación manual.
  • Aplica mínimo privilegio a la cuenta de base de datos de la aplicación.
  • Valida con listas de permitidos los fragmentos que no se pueden parametrizar (nombres de columnas, dirección de orden).
  • Desactiva los mensajes de error detallados en producción y regístralos solo en el servidor.
  • No dependas de escapar caracteres manualmente como única defensa.
  • Integra escáneres SAST/DAST en el pipeline y haz pruebas con Burp Suite, OWASP ZAP o sqlmap solo sobre sistemas autorizados.