Definición
La inyección SQL ocurre cuando una aplicación construye una consulta a la base de datos pegando dentro texto que viene del usuario. Si ese texto contiene fragmentos de SQL, pasan a formar parte de la consulta y cambian lo que hace.
El fallo de fondo es que el motor no puede distinguir tu instrucción del dato del usuario: le llega todo como una sola cadena de texto. Si el usuario consigue colar sintaxis, esa sintaxis se ejecuta con los permisos de tu aplicación.
Lleva más de veinte años en la lista de los fallos más graves de la web, y sigue apareciendo. La buena noticia: es de los pocos problemas de seguridad con una solución completa y definitiva.
Cómo funciona
Una consulta construida a mano:
// VULNERABLE — nunca hagas esto
$sql = "SELECT * FROM clientes WHERE email = '" . $_POST['email'] . "'";
Si alguien envía como email ' OR '1'='1, la consulta que llega al motor es:
SELECT * FROM clientes WHERE email = '' OR '1'='1'
Como '1'='1' es siempre verdadero, devuelve todos los clientes. En un formulario de acceso, eso puede ser entrar sin contraseña.
Y hay variantes peores: cerrar la consulta con un punto y coma y añadir otra, unir con UNION los datos de otra tabla, o —cuando la aplicación no muestra el resultado— deducir información midiendo cuánto tarda en responder.
La solución: consultas preparadas
La única defensa completa es no construir la consulta pegando texto. Se usan consultas preparadas: mandas la instrucción con huecos y los valores por separado.
$st = $pdo->prepare('SELECT * FROM clientes WHERE email = ?');
$st->execute([$_POST['email']]);
Aquí el motor recibe primero la estructura de la consulta y después los datos. Ya sabe dónde acaba la instrucción, así que lo que llegue como valor se trata como valor pase lo que pase. Aunque el usuario mande ' OR '1'='1, se buscará literalmente un cliente cuyo email sea esa cadena.
No es "más seguro": es que el problema deja de existir estructuralmente.
Ejemplo práctico
En el CRM de IMDICA todas las consultas van preparadas. Es la norma, sin excepciones, y hay una razón práctica además de la seguridad: escribir la consulta con huecos también evita errores tontos con comillas y con valores nulos.
Un fallo con el que me peleé y que sale precisamente de usarlas: error HY093, número de parámetros incorrecto. Ocurría al reutilizar el mismo hueco con nombre dos veces en la misma consulta. Con huecos con nombre, algunos motores exigen un valor por cada aparición, no por cada nombre distinto. La solución fue usar nombres distintos aunque el valor fuera el mismo.
Lo cuento porque es representativo: los problemas de las consultas preparadas son molestias de sintaxis. Los problemas de concatenar texto son fugas de datos.
Lo que no cuenta como protección:
- Escapar comillas a mano. Hay codificaciones y casos límite que se escapan de cualquier función de escape casera.
- Validar en el navegador. Se salta con cualquier herramienta.
- Filtrar palabras como
SELECToDROP. Se rodea con mayúsculas mezcladas, comentarios o codificación. - Usar un ORM sin más. Ayuda mucho porque prepara por ti, y casi todos permiten SQL en crudo. Ahí vuelves al punto de partida.
Defensa en profundidad
Las consultas preparadas resuelven el problema. Estas capas reducen el daño si algo falla:
- Permisos mínimos. El usuario de base de datos de la web no necesita poder borrar tablas.
- Validar el tipo. Si esperas un número, conviértelo a número antes de nada.
- Lista blanca para lo que no se puede parametrizar. Los nombres de columna en un
ORDER BYno admiten hueco: compáralos contra una lista de valores permitidos. - No mostrar errores de base de datos al usuario. Un mensaje del motor le dice al atacante cómo son tus tablas.
- Un cortafuegos de aplicación delante, como red adicional.
Cuándo preocuparte
Siempre que un dato que viene de fuera acabe cerca de una consulta. Y "de fuera" es más de lo que parece: formularios, parámetros de la dirección, cabeceras, cookies, ficheros subidos, respuestas de una API de terceros.
La regla de oro: si el valor no lo has escrito tú en el código, va parametrizado.