Prevenir la inyección SQL en PHP [Ejemplo completo]
¿Qué es la inyección SQL y cómo prevenirla en sistemas PHP?
Prevenir la inyección SQL en PHP. Hoy en día los ataque bajo la modalidad de inyección SQL es muy común en el mundo de la programación con bases de datos. Por lo tanto, debemos de configurar barreras y evitar esos ataques con estrategias que detallaremos en este artículo.
Recordemos que, las sentencias dinámicas que se crean como cadenas de texto y en las que se insertan/concatenan valores obtenidos de alguna fuente (normalmente proveniente del usuario atreves de un formulario HTML5). Si no protegemos esas áreas, puede lograr que sean vulnerables a inyección SQL si no limpiamos esos campos, por ejemplo:$id_usuario = $_POST["id"]; mysql_query("SELECT * FROM clientes WHERE id = $id_usuario");
Esta consulta SQL que pusimos de ejemplo nos da una vulnerabilidad muy grave en la seguridad de una aplicación web desarrollada con PHP, simplemente bastaría que un usuario final ingresara un valor como 2; DROP TABLE usuarios;
— que al ser procesada por el formulario y enviada a destino nos devolvería la siguiente consulta SQL:
SELECT * FROM usuarios WHERE id = 2; DROP TABLE usuarios;--
Si nos damos cuenta esta consulta es totalmente valida y lo que haría seria eliminar tabla Usuarios con todos los registros que tuviera.
¿Qué es la inyección SQL?
La mejor manera para entender la inyección SQL está en su propio nombre: SQL + Inyección. La palabra «inyección» aquí no está relacionado con la medicina, sino que es el uso del verbo «inyectar«.
Vamos a suponer que estamos creando un sitio web escrito con PHP para una tienda de comercio electrónico local. Por lo tanto, tenemos que agregar un formulario de contacto para nuestros usuarios:
<form action="guardar.php" method="POST"> <label>Nombre</label> <input type="text" name="nombre"> <label>Mensaje</label> <textarea name="mensaje" rows="5"></textarea> <input type="submit" value="Enviar"> </form>
Al momento de enviar el formulario, la información se enviará al archivo guardar.php
que se encargará de procesar los datos del envío. Sin embargo, esa información se almacenará en una base de datos para que los propietarios de la tienda puedan leer los mensajes de los usuarios en su panel de control.
Veamos el ejemplo:
<?php $nombre= $_POST['nombre']; $mensaje= $_POST['mensaje']; // Compruebe si este usuario ya tiene un mensaje mysqli_query($conn, "SELECT * from mensajes where nombre = $nombre"); // Mas codigos
El script anterior verifica si el usuario ya tiene un mensaje no leído. Por lo tanto, la consulta SELECT * from mensajes where nombre = $nombre
parece bastante normal y sencillo.
Si somos programadores que recién están iniciando en el mundo de la programación, con esto, hemos abierto las puertas a un ataque directo hacia nuestra base de datos. Ahora, el atacante solo debe cumplir los siguientes puntos para poder inyectar nuestra base de datos.
- El sistema web se ejecuta en una base de datos SQL (la mayoría lo usa)
- La conexión hacia la base de datos actual tiene permisos de «editar» y «eliminar«
- Adivinar los nombres de las tablas importantes.
En el punto 3 nos da a conocer que el atacante sabe que tiene una tienda de comercio electrónico, es muy probable que esté almacenando los datos del pedido en una tabla llamado pedidos. Todo lo que el atacante necesita hacer es concatenar la consulta y el resultado sería el siguiente:
Dario; truncate pedidos;
A continuación, veamos en qué se convertirá la consulta cuando sea procesada por el PHP:
SELECT * FROM mensajes WHERE nombre = Dario; truncate pedidos;
La primera parte de la consulta tiene un error de sintaxis (no hay comillas alrededor de «Dario»), pero el punto y coma obliga a MySQL a comenzar a interpretar uno nuevo: truncate pedidos
.
El resultado sería, que tendríamos una tabla llamado pedidos completamente vacía.
¿Cómo evitar que la inyección SQL en PHP?
Primeramente, hay que tomar medidas de prevención ante un ataque de inyección de SQL en el lenguaje PHP
No usar sentencias dinámicas ni funciones
Las funciones mysql_*
(mysql_connect
, mysql_query
, etc.) son inseguras por naturaleza y su uso no sólo no está recomendado, sino que se consideran obsoletas y se han eliminado completamente a partir de PHP7.
Asignar mínimos privilegios al usuario de conexión con la base de datos
Para el usuario de conexión con MySQL desde nuestro sistema debe tener los privilegios necesarios para realizar las acciones que necesitemos. Por lo tanto, no debemos de utilizar jamás un usuario root con acceso a todas las bases de datos ya que de esta forma estaremos dejando una gran brecha de vulnerabilidad.
Desinfectar la entrada del usuario
//Convierta toda la etiqueta HTML posible para evitar XSS $nombre= htmlentities($_POST['nombre']);
ó quizás podemos limpiar las cadenas enviadas desde el formulario usando los filtros de saneamiento nativos del PHP.
// Limpiar si estamos usando Cadenas $cadena= filter_var($envio, FILTER_SANITIZE_STRING); // Limpiar si estamos usando Email $email= filter_var($envio, FILTER_SANITIZE_EMAIL); // Limpiar si estamos usando numeros enteros $numero = filter_var($envio, FILTER_SANITIZE_NUMBER_INT);
La variable $envio
seria los campos enviados desde el formulario ya sea por el método POST o Método GET.
Para mayor información podemos acceder a la página oficial de estos filtros.
Utilizar declaraciones preparadas
El envío del formulario se podría sanear usando métodos descritos anteriormente, es más recomendable la utilización de sentencias preparadas. Por lo tanto, las sentencias preparadas te permitirán ejecutar la misma sentencia con más eficacia a la hora de procesar la información.
Existen dos alternativas y diferencias entre estas 2 opciones:
- PDO.- este se puede usar con diferentes tipos de base de datos
- MySQLi. Esta opción es exclusivamente para bases de datos MySQL.
La recomendación es usar la conexión PDO sobre MySQLi.
Usando PDO
Una de las ventajas de PDO son los marcadores de posición (indican dónde se sustituirá una cadena por su valor), se pueden definir usando un signo de interrogación (?
) o bien usando un nombre (generalmente empezando con :
).
Sin embargo, les recomiendo usar un nombre, porque sería complicado a la hora de tener múltiples variables en la consulta SQL.
Aquí dejo un ejemplo:
// la variable $pdo contendrá el objeto con la conexión PDO $pdo = new PDO('mysql:host=mihost;dbname=basedatos', "usuario", "contraseña"); $id_usuario = $_POST["id"]; $sentencia = $pdo->prepare("SELECT * FROM usuarios WHERE id = :idusuario"); $sentencia=$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $sentencia->bindParam(":idusuario", $id_usuario, PDO::PARAM_INT); $sentencia->execute();
Si observamos, :idusuario
se sustituirá por el valor de $_POST["id"]
de forma segura, y cuando hace el bind se indica que la variable es de tipo entero (PDO::PARAM_INT
). Esto forzara que la variable sea un numero entero, caso contrario no se ejecutara la consulta.
Si nuestra sentencia SQL posee varias variables, se debe incluir un único parámetro para cada uno de los valores que se usan en la sentencia.
$pdo = new PDO('mysql:host=mihost;dbname=basedatos', "usuario", "contraseña"); $id_usuario = $_POST["id"]; $sentencia = $pdo->prepare("UPDATE usuarios SET id = :idusuario WHERE id = :idusuario1"); $sentencia=$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $sentencia->bindParam(":idusuario", $id_usuario, PDO::PARAM_INT); $sentencia->bindParam(":idusuario1", $id_usuario, PDO::PARAM_INT); $sentencia->execute();
Usando MySQLi
Esta opción tiene dos interfaces: una procedural y otra orientada a objetos.
- La interfaz procedural es muy parecida a
mysql_*
, y por ello la gente que migra desdemysql_*
puede sentirse atraída por la facilidad quemysqli_*
ofrece al desarrollador. - Orientada a objetos. La segunda opción es mas segura y la recomendación es elegir por la versión POO.
Nota: Las funciones
mysqli_*
suelen ser parecidas a lasmysql_*
, en algunos casos pueden tener diferentes parámetros de entrada o diferentes salidas.
El ejemplo de la pregunta quedaría así con MySQLi en su interfaz orientada a objetos:
// en $mysqli tendremos la conexión MySQLi $mysqli = new mysqli("mihost", "usuario", "contraseña", "basedatos"); $id_usuario = $_POST["id"]; $sentencia = $mysqli->prepare("SELECT * FROM usuarios WHERE id = ?"); $sentencia->bind_param("i", $id_usuario ); $sentencia->execute();
Si notamos bien, es muy parecido a PDO (cambia un poco cómo se especifica el tipo de valor, i
para enteros y s
para cadenas, pero la idea es similar).
En la versión procedural de MySQLi, el código equivalente sería:
// en $conn tendríamos la conexión a la base de datos con MySQLi $conn = mysqli_connect("mihost", "usuario", "contraseña", "basedatos"); $id_usuario = $_POST["id"]; $sentencia = mysqli_prepare("SELECT * FROM usuarios WHERE id = ?"); mysqli_stmt_bind_param($sentencia, "i", $id_usuario); mysqli_stmt_execute($sentencia);
Configuración correcta del PDO
La conexión PDO para el acceso a la base de datos es muy recomendable pero podemos dejarnos llevar por una falsa sensación de seguridad.
Es verdad que PDO o declaraciones preparadas por MySQLi es bueno para evitar todo tipo de ataques de inyección SQL, pero hay que configurarlo correctamente.
Muchos tenemos la costumbre de copiar códigos y pegarlo en nuestro proyecto y continuar con el desarrollo, pero esta pequeña línea de código puede dejar la seguridad al suelo:
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
Esta línea lo que hace es ordenarle a PDO que emule declaraciones preparadas en lugar de utilizar realmente la función de declaraciones preparadas de la base de datos.
A continuación, PHP envía cadenas de consulta a la base de datos incluso si su código parece estar creando declaraciones preparadas y configurando parámetros. En resumen, el código es tan vulnerable a la inyección de SQL como el principio.
¿Cómo prevenir inyección de SQL?
Utilice las instrucciones preparadas , también conocidas como consultas parametrizadas.
Por ejemplo:
$stmt = $pdo->prepare('SELECT * FROM blog_posts WHERE YEAR(created) = ? AND MONTH(created) = ?'); if ($stmt->execute([$_GET['year'], $_GET['month']])) { $posts = $stmt->fetchAll(\PDO::FETCH_ASSOC); }
Las declaraciones preparadas eliminan cualquier posibilidad de inyección de SQL en su aplicación web. Por lo tanto, no importa lo que se envía a las variables $_GET
, la estructura de la consulta SQL no puede ser cambiada por un atacante (a menos que tenga PDO::ATTR_EMULATE_PREPARES
activado).
PDO mal configurado
Si usted es un desarrollador PHP que busca obtener el máximo provecho de PDO, le recomendamos que cambie dos de los valores predeterminados:
- Desactivar preparaciones emuladas. Esto asegura que obtenga declaraciones preparadas.
- Establecer el modo de error para lanzar excepciones. Esto evita que tenga que estar observando los resultados de
PDOStatement::execute()
y hace que su código sea menos redundante.
Para hacer ambas cosas:
$pdo = new PDO(/* sus credenciales de conexión */); $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Debido a que PDO::ATTR_EMULATE_PREPARES
está seteado a false , estamos obteniendo instrucciones reales preparadas, y porque hemos seteado PDO::ATTR_ERRMORE
a PDO::ERRMODE_EXCEPTION
$stmt = $pdo->prepare("SELECT * FROM foo WHERE first_name = ? AND last_name = ?"); if ($stmt->execute([$_GET['first_name'], $_GET['last_name'])) { $users = $stmt->fetchAll(PDO::FETCH_ASSOC); } else { // Handle error here. } $args = [ json_encode($_GET) (new DateTime())->format('Y-m-d H:i:s') ]; $insert = $pdo->prepare("INSERT INTO foo_log (params, time) VALUES (?, ?);"); if (!$insert->execute($args)) { // Handle error here. }
Una recomendación seria escribir el código así:
try { $stmt = $pdo->prepare("SELECT * FROM foo WHERE first_name = ? AND last_name = ?"); $stmt->execute([$_GET['first_name'], $_GET['last_name']); $users = $stmt->fetchAll(PDO::FETCH_ASSOC); $args = [ json_encode($_GET), (new DateTime())->format('Y-m-d H:i:s') ]; $pdo->prepare("INSERT INTO foo_log (params, time) VALUES (?, ?);") ->execute($args); } catch (PDOException $ex) { // Handle error here. }
Con estas declaraciones obtenemos mayor seguridad, brevedad y mejor legibilidad.
EL script anterior suena innecesariamente complejo, pero el concepto bien vale la pena el esfuerzo.
La solución es simple: asegúrese de que esta emulación esté configurada como falsa.
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Ahora, el script PHP se ve obligado a usar declaraciones preparadas a nivel de base de datos, lo que evita todo tipo de inyección SQL.
Conclusión
Para no tener momentos desagradables en un futuro es mejor prevenir y en este articulo hemos aprendido a evitar la famosa técnica llamado «inyección SQL«, recuerda que es un ataque muy desagradable a una aplicación web, pero se evita fácilmente aplicando técnicas concretas.
Siempre hay que tener cuidado al procesar la entrada del usuario (por cierto, SQL Injection no es la única amenaza, existen varios y lo detallaremos en otro articulo.
Espero que esta explicación les ayude a proteger sus sistemas web.