Pregunta ¿Cómo puedo prevenir la inyección SQL en PHP?


Si la entrada del usuario se inserta sin modificación en una consulta SQL, entonces la aplicación se vuelve vulnerable a inyección SQL, como en el siguiente ejemplo:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

Eso es porque 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?


2781


origen


Respuestas:


Use declaraciones preparadas y consultas parametrizadas. Estas son sentencias SQL que el servidor de la base de datos envía y analiza independientemente de cualquier parámetro. De esta forma, es imposible para un atacante inyectar SQL malicioso.

Básicamente tienes dos opciones para lograr esto:

  1. Utilizando DOP (para cualquier controlador de base de datos compatible):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('name' => $name));
    
    foreach ($stmt as $row) {
        // do something with $row
    }
    
  2. Utilizando MySQLi (para MySQL):

    $stmt = $dbConnection->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 es MySQL, hay una segunda opción específica del controlador a la que puede hacer referencia (p. pg_prepare() y pg_execute() para PostgreSQL). PDO es la opción universal.

Configurar correctamente la conexión

Tenga en cuenta que al usar PDO para acceder a una base de datos MySQL real declaraciones preparadas son no utilizado por defecto. Para solucionar esto, debe desactivar la emulación de las declaraciones preparadas. Un ejemplo de crear una conexión usando PDO es:

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

$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 aconseja agregarlo. De esta forma, el script no se detendrá con un Fatal Error cuando algo sale mal Y le da al desarrollador la oportunidad de catch cualquier error que sea thrown como PDOExceptions.

Que es obligatorio, sin embargo, es el primero setAttribute() línea, que le dice a PDO que deshabilite las declaraciones preparadas emuladas y el uso real declaraciones preparadas. Esto asegura que PHP no analiza la declaración y los valores antes de enviarlos al servidor MySQL (lo que da al posible atacante la posibilidad de inyectar SQL malicioso).

Aunque puedes configurar el charset en las opciones del constructor, es importante tener en cuenta que las versiones 'anteriores' de PHP (<5.3.6) silenciosamente ignoró el parámetro del juego de caracteres en el DSN.

Explicación

Lo que ocurre es que la instrucción SQL que pasa a prepare es analizado y compilado por el servidor de la base de datos. Al especificar los parámetros (ya sea un ? o un parámetro nombrado como :name en el ejemplo anterior) le dice al motor de la base de datos donde desea filtrar. Entonces cuando llamas execute, la instrucción preparada se combina con los valores de 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 de SQL. La inyección SQL funciona al engañar al script para que incluya cadenas maliciosas cuando crea SQL para enviar a la base de datos. Entonces al enviar el SQL real por separado de los parámetros, usted limita el riesgo de terminar con algo que no pretendía. Cualquier parámetro que envíe al usar una declaración preparada simplemente se tratará como cadenas (aunque el motor de la base de datos puede hacer algo de optimización para que los parámetros también puedan terminar como números, por supuesto). En el ejemplo anterior, si el $namevariable contiene 'Sarah'; DELETE FROM employees el resultado sería simplemente una búsqueda de la cadena "'Sarah'; DELETE FROM employees"y no terminarás con una mesa vacía.

Otro beneficio del uso de 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 dará algunas ganancias de velocidad.

Ah, y ya que preguntaste cómo hacerlo para insertar, aquí hay un ejemplo (usando PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute(array('column' => $unsafeValue));

¿Se pueden usar declaraciones preparadas para consultas dinámicas?

Si bien aún puede usar 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 consulta no se pueden parametrizar.

Para estos escenarios específicos, lo mejor que puede hacer es usar 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';
}

7938



Advertencia:   El código de muestra de esta respuesta (como el código de muestra de la pregunta) usa PHP mysql extensión, que fue desaprobada en PHP 5.5.0 y eliminada por completo en PHP 7.0.0.

Si está usando una versión reciente de PHP, mysql_real_escape_string La opción que se detalla a continuación ya no estará disponible (aunque mysqli::escape_string es un equivalente moderno). En estos días el mysql_real_escape_string la opción solo tendría sentido para el código heredado en una versión anterior de PHP.


Tienes dos opciones: escapar de los personajes especiales en tu unsafe_variable, o usando una consulta parametrizada. Ambos lo protegerían de la inyección de SQL. La consulta parametrizada se considera la mejor práctica, pero requerirá cambiar a una nueva extensión mysql en PHP antes de poder usarla.

Cubriremos la cadena de impacto más baja escapando primero.

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

Ver también, los detalles de la mysql_real_escape_string función.

Para usar la consulta parametrizada, necesita usar MySQLi en lugar de la MySQL funciones. Para volver a escribir su ejemplo, necesitaríamos algo como lo siguiente.

<?php
    $mysqli = new mysqli("server", "username", "password", "database_name");

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

La función clave que querrá leer aquí sería mysqli::prepare.

Además, como otros han sugerido, puede que le resulte útil / más fácil intensificar una capa de abstracción con algo así como DOP.

Tenga en cuenta que el caso sobre el que preguntó es bastante simple y que casos más complejos pueden requerir enfoques más complejos. En particular:

  • Si desea modificar la estructura del SQL según 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 los valores 'seguros'.
  • Si usa números enteros de la entrada del usuario en una condición y toma el mysql_real_escape_string enfoque, sufrirá el problema descrito por Polinomio en los comentarios a continuación. Este caso es más complicado porque los enteros no estarían rodeados de comillas, por lo que podría tratar validando que la entrada del usuario contenga solo dígitos.
  • Es probable que haya otros casos de los que no tengo conocimiento. Usted puede encontrar esta es un recurso útil sobre algunos de los problemas más sutiles que puede encontrar.

1509



Cada respuesta aquí cubre solo una parte del problema.
De hecho, hay cuatro diferentes partes de consulta que podemos agregar de forma dinámica:

  • una cuerda
  • un número
  • un identificador
  • una palabra clave de sintaxis

y las declaraciones preparadas cubren solo 2 de ellas

Pero a veces tenemos que hacer que nuestra consulta sea aún más dinámica, agregando operadores o identificadores también.
Entonces, necesitaremos diferentes técnicas de protección.

En general, dicho enfoque de protección se basa en lista blanca. En este caso, todos los parámetros dinámicos deben estar codificados en su secuencia de comandos y elegidos de ese conjunto.
Por ejemplo, para hacer un pedido dinámico:

$orders  = array("name","price","qty"); //field names
$key     = array_search($_GET['sort'],$orders)); // see if we have such a name
$orderby = $orders[$key]; //if not, first one will be set automatically. smart enuf :)
$query   = "SELECT * FROM `table` ORDER BY $orderby"; //value is safe

Sin embargo, hay otra forma de proteger los identificadores: escapar. Siempre que tengas un identificador citado, puedes escapar de los palos de atrás en el interior al doblarlos.

Como un paso más, podemos tomar prestada una idea realmente brillante de usar algún marcador de posición (un proxy para representar el valor real en la consulta) de las declaraciones preparadas e inventar un marcador de posición de otro tipo - un marcador de posición identificador.

Entonces, para abreviar la larga historia: es una marcador de posiciónno declaración preparada puede ser considerado como una bala de plata.

Por lo tanto, una recomendación general puede formularse como
Siempre que agregue partes dinámicas a la consulta utilizando marcadores de posición (y estos marcadores de posición se procesan correctamente por supuesto), puede estar seguro de que su consulta es segura..

Aún así, hay un problema con las palabras clave de sintaxis de SQL (como AND, DESC y tal), pero la inclusión en la lista blanca parece ser el único enfoque en este caso.

Actualizar

Aunque existe un acuerdo general sobre las mejores prácticas con respecto a la protección de inyección SQL, existen Todavía hay muchas malas prácticas también. Y algunos de ellos están muy arraigados en la mente de los usuarios de PHP. Por ejemplo, en esta misma página hay (aunque invisible para la mayoría de los visitantes) más de 80 respuestas eliminadas - todo eliminado por la comunidad debido a mala calidad o promoción de prácticas malas y desactualizadas. Peor aún, algunas de las malas respuestas no se eliminan, sino que prosperan.

Por ejemplo, allí (1)  son (2)  todavía (3)  muchos (4)  respuestas (5), incluyendo la segunda respuesta más votada sugiriéndole escaparse manualmente de cuerdas, un enfoque obsoleto que se ha demostrado inseguro.

O hay una respuesta ligeramente mejor que sugiere solo otro método de formateo de cadenas e incluso lo jacta como la última panacea. Si bien, por supuesto, no lo es. Este método no es mejor que el formato de cadena regular, pero conserva todos sus inconvenientes: se aplica únicamente a las cadenas y, como cualquier otro formateo manual, es esencialmente opcional, no obligatorio, propenso a errores humanos de cualquier tipo.

Creo que todo esto debido a una superstición muy antigua, respaldada por autoridades como OWASP o Manual de PHP, que proclama la igualdad entre lo que "escapa" y la protección de las inyecciones de SQL.

Independientemente de lo que el manual de PHP haya dicho por años, *_escape_string de ninguna manera hace que los datos sean seguros y nunca fue pensado. Además de ser inútil para cualquier parte de SQL que no sea una cadena, el escapado manual es incorrecto porque es manual, lo contrario que automatizado.

Y OWASP lo empeora aún más, haciendo hincapié en escapar entrada del usuario lo cual es una completa tontería: no debería haber tales palabras en el contexto de la protección de inyección. Cada variable es potencialmente peligrosa, ¡sin importar la fuente! O, en otras palabras, todas las variables deben estar formateadas adecuadamente para ser puestas en una consulta, sin importar la fuente de nuevo. 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 el primer paso hacia el desastre. Sin mencionar que incluso la redacción sugiere un escape masivo en el punto de entrada, que se asemeja a la función de comillas muy mágicas, ya despreciadas, obsoletas y eliminadas.

Entonces, a diferencia de lo que escapó, declaraciones preparadas es la medida que de hecho protege de la inyección SQL (cuando corresponda).

Si todavía no estás convencido, aquí hay una explicación paso a paso que escribí, La guía del autoestopista para la prevención de inyección SQL, donde expliqué todos estos asuntos en detalle e incluso compilé una sección dedicada por completo a las malas prácticas y su divulgación.


940



Yo recomendaría usar DOP (Objetos de datos PHP) para ejecutar consultas SQL parametrizadas.

Esto no solo protege contra la inyección de SQL, sino que también acelera las consultas.

Y al usar PDO en lugar de mysql_, mysqli_y pgsql_ funciones, usted hace que su aplicación sea un poco más abstracta de la base de datos, en el caso poco frecuente de que deba cambiar de proveedor de bases de datos.


768



Utilizar PDO y consultas preparadas.

($conn es un PDO objeto)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

565



Como puede ver, las personas sugieren que use declaraciones preparadas como máximo. No está mal, pero cuando se ejecuta su consulta sólo una vez por proceso, habría una pequeña penalización de rendimiento.

Estaba enfrentando este problema, pero creo que lo resolví en muy forma sofisticada: la forma en que los piratas informáticos utilizan para evitar el uso de comillas. Usé esto junto con declaraciones preparadas emuladas. Lo uso para prevenir todas tipos de posibles ataques de inyección SQL.

Mi acercamiento:

  • Si espera que la entrada sea entera, asegúrese de que sea De Verdad entero. En un lenguaje de tipo variable como PHP, es esto muy importante. Puede usar, por ejemplo, esta solución muy simple pero poderosa: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input); 

  • Si esperas algo más de un entero hexagonal. Si lo haces, escaparás perfectamente de todas las entradas. En C / C ++ hay una función llamada mysql_hex_string(), en PHP puedes usar bin2hex().

    No te preocupes porque la cadena escapada tendrá un tamaño 2x de su longitud original porque incluso si usas mysql_real_escape_string, PHP tiene que asignar la misma capacidad ((2*input_length)+1), que es lo mismo

  • Este método hexadecimal a menudo se usa cuando transfieres datos binarios, pero no veo ninguna razón para no usarlo en todos los datos para evitar ataques de inyección SQL. Tenga en cuenta que debe anteponer los datos con 0x o usa la función MySQL UNHEX en lugar.

Entonces, por ejemplo, la consulta:

SELECT password FROM users WHERE name = 'root'

Se convertirá:

SELECT password FROM users WHERE name = 0x726f6f74

o

SELECT password FROM users WHERE name = UNHEX('726f6f74')

Hex es el escape perfecto. No hay forma de inyectar

Diferencia entre la función UNHEX y el prefijo 0x

Hubo algo de discusión en los comentarios, así que finalmente quiero dejarlo en claro. Estos dos enfoques son muy similares, pero son un poco diferentes en algunos aspectos:

El prefijo ** 0x ** solo se puede usar para columnas de datos como char, varchar, texto, bloque, binario, etc..
Además, su uso es un poco complicado si está por insertar una cadena vacía. Tendrás que reemplazarlo por completo con '', o obtendrás un error.

UNHEX () trabaja en alguna columna; no tiene que preocuparse por la cadena vacía.


Los métodos hexadecimales a menudo se usan como ataques

Tenga en cuenta que este método hexadecimal a menudo se utiliza como un ataque de inyección SQL donde los enteros son como cadenas y se escaparon solo con mysql_real_escape_string. Entonces puedes evitar el uso de comillas.

Por ejemplo, si solo haces algo como esto:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

un ataque puede inyectarte muy fácilmente. Considere el siguiente código inyectado devuelto por su script:

SELECCIONAR ... DONDE id = -1 union all seleccionar table_name de information_schema.tables

y ahora solo extrae la estructura de la tabla:

SELECCIONAR ... DONDE id = -1 unión seleccionar select column_name de information_schema.column donde table_name = 0x61727469636c65

Y luego solo selecciona los datos que desees. ¿No es genial?

Pero si el codificador de un sitio inyectable lo hexagonal, no sería posible inyectar porque la consulta se vería así: SELECT ... WHERE id = UNHEX('2d312075...3635')


498



IMPORTANTE

La mejor manera de prevenir la inyección de SQL es usar Declaraciones preparadas  en lugar de escapar, como la respuesta aceptada demuestra.

Hay bibliotecas como Aura.Sql y EasyDB que permiten a los desarrolladores usar declaraciones preparadas más fácilmente. Para obtener más información sobre por qué las declaraciones preparadas son mejores en detener la inyección de SQL, Referirse a esta mysql_real_escape_string() derivación y solucionó recientemente las vulnerabilidades de Inyección SQL Unicode en WordPress.

Prevención de inyecciones mysql_real_escape_string ()

PHP tiene una función especialmente diseñada para prevenir estos ataques. Todo lo que necesitas hacer es usar el bocado de una función, mysql_real_escape_string.

mysql_real_escape_string toma una cadena que se usará en una consulta MySQL y devuelve la misma cadena con todos los intentos de inyección SQL escapados de forma segura. Básicamente, reemplazará esas citas problemáticas (') un usuario podría ingresar con un sustituto seguro de MySQL, una cita escapada \'.

NOTA: ¡debe estar conectado a la base de datos para usar esta función!

// Conectarse a MySQL

$name_bad = "' OR 1'"; 

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '"; 

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

Puedes encontrar más detalles en MySQL - Prevención de inyección SQL.


452



Advertencia de seguridad: Esta respuesta no está en línea con las mejores prácticas de seguridad. El escape es inadecuado para evitar la inyección de SQL, utilizar declaraciones preparadas en lugar. Use la estrategia descrita debajo bajo su propio riesgo. (También, mysql_real_escape_string() fue eliminado en PHP 7.)

Podrías hacer algo básico como esto:

$safe_variable = mysql_real_escape_string($_POST["user-input"]);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

Esto no resolverá todos los problemas, pero es un buen trampolín. Olvidé elementos obvios como verificar la existencia de la variable, el formato (números, letras, etc.).


410



Lo que sea que termine usando, asegúrese de verificar que su entrada no haya sido destruida por magic_quotes o alguna otra basura bien intencionada, y si es necesario, ejecutarla stripslashes o lo que sea para desinfectarlo.


345



La consulta parametrizada Y la validación de entrada es el camino a seguir. Hay muchos escenarios en los que se puede producir inyección SQL, aunque mysql_real_escape_string() ha sido usado.

Esos ejemplos son vulnerables a la inyección SQL:

$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");

o

$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");

En ambos casos, no puedes usar ' para proteger la encapsulación

Fuente: La inyección inesperada de SQL (cuando escaparse no es suficiente)


328



En mi opinión, la mejor manera de evitar la inyección de SQL en su aplicación PHP (o cualquier aplicación web, en realidad) es pensar en la arquitectura de su aplicación. Si la única forma de protegerse contra la inyección de SQL es recordar utilizar un método o función especial que haga Lo correcto cada vez que hable con la base de datos, lo está haciendo mal. De esta forma, solo es cuestión de tiempo hasta que te olvides de formatear correctamente tu consulta en algún punto de tu código.

Adoptando el patrón MVC y un marco como CakePHP o CodeIgniter probablemente sea el camino correcto: las tareas comunes como crear consultas de bases de datos seguras se han resuelto e implementado de forma centralizada en dichos marcos. Te ayudan a organizar tu aplicación web de una manera sensata y te hacen pensar más sobre la carga y el almacenamiento de objetos que sobre la construcción segura de consultas SQL individuales.


280