¿Cómo puedo evitar la inyección de SQL en PHP?
Si la entrada del usuario se inserta sin modificaciones en una consulta SQL, entonces la aplicación se vuelve vulnerable a la inyección SQL , como en el siguiente ejemplo:
$unsafe_variable = $_POST['user_input'];
mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
Esto se debe a que el usuario puede ingresar algo como value'); DROP TABLE table;--
y la consulta se convierte en:
INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')
¿Qué se puede hacer para evitar que esto suceda?
La forma correcta de evitar ataques de inyección SQL, sin importar qué base de datos utilice, es separar los datos de SQL , de modo que los datos sigan siendo datos y nunca sean interpretados como comandos por el analizador SQL. Es posible crear una declaración SQL con partes de datos formateadas correctamente, pero si no comprende completamente los detalles, siempre debe utilizar declaraciones preparadas y consultas parametrizadas. Estas son sentencias SQL que el servidor de la base de datos envía y analiza por separado de cualquier parámetro. De esta forma, es imposible que un atacante inyecte SQL malicioso.
Básicamente tienes dos opciones para lograr esto:
Usando PDO (para cualquier controlador de base de datos compatible):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute([ 'name' => $name ]); foreach ($stmt as $row) { // Do something with $row }
Usando MySQLi (para MySQL):
desde PHP 8.2+ podemos hacer uso delexecute_query()
cual prepara, vincula parámetros y ejecuta declaraciones SQL en un solo método:$result = $db->execute_query('SELECT * FROM employees WHERE name = ?', [$name]); while ($row = $result->fetch_assoc()) { // Do something with $row }
Hasta PHP8.1:
$stmt = $db->prepare('SELECT * FROM employees WHERE name = ?'); $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string' $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { // Do something with $row }
Si se está conectando a una base de datos que no sea MySQL, hay una segunda opción específica del controlador a la que puede consultar (por ejemplo, pg_prepare()
y pg_execute()
para PostgreSQL). La DOP es la opción universal.
Configurar correctamente la conexión
DOP
Tenga en cuenta que cuando se utiliza PDO para acceder a una base de datos MySQL, no se utilizan declaraciones reales preparadas de forma predeterminada . Para solucionar este problema, debe desactivar la emulación de declaraciones preparadas. Un ejemplo de creación de una conexión utilizando PDO es:
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8mb4', 'user', 'password');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
En el ejemplo anterior, el modo de error no es estrictamente necesario, pero se recomienda agregarlo . De esta manera PDO le informará de todos los errores de MySQL lanzando el archivo PDOException
.
Lo que es obligatorio , sin embargo, es la primera setAttribute()
línea, que le indica a PDO que deshabilite las declaraciones preparadas emuladas y utilice declaraciones preparadas reales . Esto asegura que PHP no analice la declaración y los valores antes de enviarlos al servidor MySQL (sin darle a un posible atacante ninguna posibilidad de inyectar SQL malicioso).
Aunque puedes configurarlo charset
en las opciones del constructor, es importante tener en cuenta que las versiones "antiguas" de PHP (anteriores a 5.3.6) ignoraron silenciosamente el parámetro charset en el DSN.
mysqli
Para mysqli tenemos que seguir la misma rutina:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // error reporting
$dbConnection = new mysqli('127.0.0.1', 'username', 'password', 'test');
$dbConnection->set_charset('utf8mb4'); // charset
Explicación
La declaración SQL que pasa prepare
es analizada y compilada por el servidor de la base de datos. Al especificar parámetros (ya sea un parámetro ?
o un parámetro con nombre como :name
en el ejemplo anterior), le indica al motor de base de datos dónde desea filtrar. Luego, cuando llama execute
, la declaración preparada se combina con los valores de los parámetros que especifique.
Lo importante aquí es que los valores de los parámetros se combinan con la declaración compilada, no con una cadena SQL. La inyección de SQL funciona engañando al script para que incluya cadenas maliciosas cuando crea SQL para enviarlo a la base de datos. Entonces, al enviar el SQL real por separado de los parámetros, limita el riesgo de terminar con algo que no pretendía.
Cualquier parámetro que envíe cuando utilice una declaración preparada simplemente será tratado como cadena (aunque el motor de la base de datos puede realizar alguna optimización para que los parámetros también terminen como números, por supuesto). En el ejemplo anterior, si la $name
variable contiene 'Sarah'; DELETE FROM employees
el resultado sería simplemente una búsqueda de la cadena "'Sarah'; DELETE FROM employees"
, y no terminará con una tabla vacía .
Otro beneficio de usar declaraciones preparadas es que si ejecuta la misma declaración muchas veces en la misma sesión, solo se analizará y compilará una vez, lo que le brindará algunas ganancias de velocidad.
Ah, y como preguntaste cómo hacerlo para una inserción, aquí tienes un ejemplo (usando PDO):
$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');
$preparedStatement->execute([ 'column' => $unsafeValue ]);
¿Se pueden utilizar declaraciones preparadas para consultas dinámicas?
Si bien aún puede utilizar declaraciones preparadas para los parámetros de consulta, la estructura de la consulta dinámica en sí no se puede parametrizar y ciertas características de la consulta no se pueden parametrizar.
Para estos escenarios específicos, lo mejor que se puede hacer es utilizar un filtro de lista blanca que restrinja los valores posibles.
// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}
Para utilizar la consulta parametrizada, debe utilizar Mysqli o PDO. Para reescribir su ejemplo con mysqli, necesitaríamos algo como lo siguiente.
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("server", "username", "password", "database_name");
$variable = $_POST["user-input"];
$stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");
// "s" means the database expects a string
$stmt->bind_param("s", $variable);
$stmt->execute();
La función clave sobre la que querrás leer sería mysqli::prepare
.
Además, como han sugerido otros, puede que le resulte útil/más fácil aumentar una capa de abstracción con algo como PDO .
Tenga en cuenta que el caso sobre el que preguntó es bastante simple y que los casos más complejos pueden requerir enfoques más complejos. En particular:
- Si desea modificar la estructura del SQL en función de la entrada del usuario, las consultas parametrizadas no ayudarán y el escape requerido no está cubierto por
mysql_real_escape_string
. En este tipo de caso, sería mejor pasar la entrada del usuario a través de una lista blanca para garantizar que solo se permitan valores "seguros".
Cada respuesta aquí cubre sólo una parte del problema. De hecho, hay cuatro partes de consulta diferentes que podemos agregar a SQL dinámicamente: -
- una cuerda
- un número
- un identificador
- una palabra clave de sintaxis
Y las declaraciones preparadas cubren sólo dos de ellos.
Pero a veces tenemos que dinamizar aún más nuestra consulta, añadiendo también operadores o identificadores. Entonces, necesitaremos diferentes técnicas de protección.
En general, este enfoque de protección se basa en listas blancas .
En este caso, cada parámetro dinámico debe estar codificado en su secuencia de comandos y elegido de ese conjunto. Por ejemplo, para realizar pedidos dinámicos:
$orders = array("name", "price", "qty"); // Field names
$key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically.
$query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe
Para facilitar el proceso, escribí una función auxiliar de lista blanca que hace todo el trabajo en una sola línea:
$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe
Hay otra forma de proteger los identificadores: escapar, pero prefiero utilizar la lista blanca como un enfoque más sólido y explícito. Sin embargo, siempre que tenga un identificador entre comillas, puede omitir el carácter de comillas para que sea seguro. Por ejemplo, de forma predeterminada para MySQL, debe duplicar el carácter de comilla para escapar . Para otros DBMS, las reglas de escape serían diferentes.
Aún así, existe un problema con las palabras clave de sintaxis SQL (como AND
y DESC
tales), pero la inclusión en listas blancas parece el único enfoque en este caso.
Por lo tanto, una recomendación general puede formularse como
- Cualquier variable que represente un literal de datos SQL (o, para decirlo simplemente, una cadena SQL o un número) debe agregarse mediante una declaración preparada. Sin excepciones.
- Cualquier otra parte de la consulta, como una palabra clave SQL, una tabla o un nombre de campo, o un operador, debe filtrarse a través de una lista blanca.
Actualizar
Aunque existe un acuerdo general sobre las mejores prácticas con respecto a la protección de inyección SQL, también existen muchas malas prácticas. Y algunos de ellos están demasiado arraigados en la mente de los usuarios de PHP. Por ejemplo, en esta misma página hay (aunque invisibles para la mayoría de los visitantes) más de 80 respuestas eliminadas , todas eliminadas por la comunidad debido a su mala calidad o a la promoción de prácticas malas y obsoletas. Peor aún, algunas de las malas respuestas no se eliminan, sino que prosperan.
Por ejemplo, (1) hay (2) todavía (3) muchas (4) respuestas (5) , incluida la segunda respuesta más votada que sugiere el escape manual de cadenas, un enfoque obsoleto que ha demostrado ser inseguro.
O hay una respuesta ligeramente mejor que sugiere simplemente otro método de formateo de cadenas e incluso lo presenta como la panacea definitiva. Aunque claro, no lo es. Este método no es mejor que el formateo de cadenas normal, pero mantiene todos sus inconvenientes: es aplicable sólo a cadenas y, como cualquier otro formateo manual, es esencialmente una medida opcional, no obligatoria, propensa a errores humanos de cualquier tipo.
Creo que todo esto se debe a una superstición muy antigua, respaldada por autoridades como OWASP o el manual de PHP , que proclama la igualdad entre cualquier "escape" y la protección contra las inyecciones de SQL.
Independientemente de lo que el manual de PHP haya dicho durante años, *_escape_string
de ninguna manera hace que los datos sean seguros y nunca fue su intención. Además de ser inútil para cualquier parte de SQL que no sea una cadena, el escape manual es incorrecto, porque es manual en lugar de automatizado.
Y OWASP lo empeora aún más, haciendo hincapié en escapar de la entrada del usuario , lo cual es un completo disparate: no deberían existir tales palabras en el contexto de la protección contra inyecciones. Cada variable es potencialmente peligrosa, ¡sin importar la fuente! O, en otras palabras, cada variable debe tener el formato adecuado para incluirse en una consulta, sin importar la fuente. Es el destino lo que importa. En el momento en que un desarrollador comienza a separar las ovejas de las cabras (pensando si alguna variable en particular es "segura" o no), da su primer paso hacia el desastre. Sin mencionar que incluso la redacción sugiere un escape masivo en el punto de entrada, asemejándose a la función de comillas mágicas, que ya es despreciada, obsoleta y eliminada.
Entonces, a diferencia de cualquier "escape", las declaraciones preparadas son la medida que realmente protege contra la inyección de SQL (cuando corresponda).
Recomiendo usar PDO (objetos de datos PHP) para ejecutar consultas SQL parametrizadas.
Esto no sólo protege contra la inyección de SQL, sino que también acelera las consultas.
Y al usar PDO en lugar de las funciones mysql_
, mysqli_
y pgsql_
, hace que su aplicación esté un poco más abstraída de la base de datos, en el raro caso de que tenga que cambiar de proveedor de base de datos.