Pregunta

Si la entrada del usuario se inserta sin modificaciones 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?

¿Fue útil?

Solución

Utilice 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:

  1. Usando 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. Usando 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 sea MySQL, hay una segunda opción específica del controlador a la que puede consultar (p. ej. pg_prepare() y pg_execute() para PostgreSQL).La DOP es la opción universal.

Configurar correctamente la conexión

Tenga en cuenta que al utilizar PDO para acceder a una base de datos MySQL real declaraciones preparadas son no utilizado por defecto.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=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 recomienda agregarlo.De esta manera el guión no se detendrá con un Fatal Error cuando algo sale mal.Y le da al desarrollador la oportunidad de catch cualquier error que sea thrownorte como PDOExceptions.

Qué es obligatorio, sin embargo, es el primero setAttribute() línea, que le dice a PDO que deshabilite las declaraciones preparadas emuladas y use real declaraciones preparadas.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 configurar el charset en las opciones del constructor, es importante tener en cuenta que las versiones 'antiguas' de PHP (< ​​5.3.6) ignoró silenciosamente el parámetro charset en el DSN.

Explicación

Lo que sucede es que la sentencia SQL que le pasas prepare es analizado y compilado por el servidor de base de datos.Al especificar parámetros (ya sea un ? o un parámetro con nombre como :name en el ejemplo anterior) le indica al motor de base de datos dónde desea filtrar.Entonces cuando llames execute, la declaración preparada se combina con los valores de parámetro 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 el $name la variable 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 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(array('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';
}

Otros consejos

Advertencia obsoleta:El código de muestra de esta respuesta (como el código de muestra de la pregunta) usa PHP MySQL extensión, que quedó obsoleta en PHP 5.5.0 y se eliminó por completo en PHP 7.0.0.

Advertencia de seguridad:Esta respuesta no está en línea con las mejores prácticas de seguridad. Escapar no es adecuado para evitar la inyección de SQL, usar declaraciones preparadas en cambio.Utilice la estrategia que se describe a continuación bajo su propia responsabilidad.(También, mysql_real_escape_string() fue eliminado en PHP 7.)

Si está utilizando una versión reciente de PHP, el mysql_real_escape_string La opción que se describe 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 sólo tendría sentido para código heredado en una versión antigua de PHP.


Tienes dos opciones: escapar de los caracteres especiales en tu unsafe_variable, o utilizando una consulta parametrizada.Ambos lo protegerían de la inyección SQL.La consulta parametrizada se considera la mejor práctica, pero será necesario cambiar a una extensión MySQL más nueva en PHP antes de poder usarla.

Primero cubriremos la cuerda de impacto inferior que se escapa.

//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 utilizar la consulta parametrizada, debe utilizar MySQLi en lugar del mysql funciones.Para reescribir 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 sobre la que querrás leer sería mysqli::prepare.

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

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".
  • Si utiliza 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 números enteros no estarían entre comillas, por lo que podría solucionarlo validando que la entrada del usuario contenga solo dígitos.
  • Probablemente hay otros casos que no conozco.Podrías encontrar este es un recurso útil sobre algunos de los problemas más sutiles que puede encontrar.

Cada respuesta aquí cubre sólo una parte del problema.De hecho, hay cuatro diferentes partes de consulta que podemos agregarle 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 lista blanca.

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)); // 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, existe otra forma de proteger los identificadores: escapar.Siempre que tenga un identificador entrecomillado, puede escapar de las comillas invertidas duplicándolas.

Como paso adicional, podemos tomar prestada una idea verdaderamente brillante de utilizar 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 de identificador.

Entonces, para resumir la historia:es un marcador de posición, no declaración preparada puede considerarse como una solución milagrosa.

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

Aún así, existe un problema con las palabras clave de sintaxis SQL (como AND, DESC y demás), pero la inclusión en una 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 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 - todo eliminado por la comunidad debido a mala calidad o promoción de malas prácticas u obsoletas.Peor aún, algunas de las malas respuestas no se eliminan, sino que prosperan.

Por ejemplo, ahí(1) son(2) todavía(3) muchos(4) respuestas(5), incluyendo el segunda respuesta más votada sugiriéndole 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 formato de cadena e incluso se jacta de ello 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 formato manual, es esencialmente una medida opcional y no obligatoria, propensa a errores humanos de cualquier tipo.

Creo que todo esto se debe a una superstición muy antigua, apoyada por autoridades como OWASP o el manual de PHP, que proclama la igualdad entre cualquier "escape" y la protección contra inyecciones de SQL.

Independientemente de lo que dijo el manual de PHP durante años, *_escape_string de ninguna manera hace que los datos sean seguros y nunca ha sido la 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. 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 "escapada", declaraciones preparadas es la medida que efectivamente protege de la inyección SQL (cuando corresponda).

Si aún no estás convencido, aquí tienes una explicación paso a paso que escribí. La guía del autoestopista para la prevención de la inyección SQL, donde expliqué detalladamente todas estas cuestiones e incluso compilé un apartado íntegramente dedicado a las malas prácticas y su divulgación.

Recomiendo usar DOP (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 utilizar PDO en lugar de mysql_, mysqli_, y pgsql_ funciones, 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.

Usar 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();

Como puede ver, la gente sugiere que utilice, como máximo, declaraciones preparadas.No está mal, pero cuando se ejecuta tu consulta sólo una vez por proceso, habría una ligera penalización en el rendimiento.

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

Mi acercamiento:

  • Si espera que la entrada sea un número entero, asegúrese de que sea en realidad entero.En un lenguaje de tipo variable como PHP es esto muy importante.Puedes utilizar, por ejemplo, esta solución muy sencilla pero potente: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

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

    No se preocupe porque la cadena de escape tendrá un tamaño 2x de su longitud original porque incluso si usa mysql_real_escape_string, PHP tiene que asignar la misma capacidad ((2*input_length)+1), que es lo mismo.

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

Así, 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 cierta discusión en los comentarios, así que finalmente quiero dejarlo claro.Estos dos enfoques son muy similares, pero son un poco diferentes en algunos aspectos:

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

UNHEX() trabaja en cualquier columna;no tienes que preocuparte por la cadena vacía.


Los métodos hexadecimales se utilizan a menudo como ataques.

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

Por ejemplo, si 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 identificación = -1 unión todos seleccionan nombre_tabla de información_esquema.tables

y ahora simplemente extrae la estructura de la tabla:

SELECCIONAR ...DONDE id = -1 unión todos seleccionan nombre_columna de esquema_información.columna donde nombre_tabla = 0x61727469636c65

Y luego simplemente seleccione los datos que desee.¿No es genial?

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

Advertencia obsoleta:El código de muestra de esta respuesta (como el código de muestra de la pregunta) usa PHP MySQL extensión, que quedó obsoleta en PHP 5.5.0 y se eliminó por completo en PHP 7.0.0.

Advertencia de seguridad:Esta respuesta no está en línea con las mejores prácticas de seguridad. Escapar no es adecuado para evitar la inyección de SQL, usar declaraciones preparadas en cambio.Utilice la estrategia que se describe a continuación bajo su propia responsabilidad.(También, mysql_real_escape_string() fue eliminado en PHP 7.)

IMPORTANTE

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

Hay bibliotecas como Aura.Sql y fácildb que permiten a los desarrolladores utilizar declaraciones preparadas más fácilmente.Para obtener más información sobre por qué las declaraciones preparadas son mejores para detener la inyección SQL, Referirse a este mysql_real_escape_string() derivación y Recientemente se corrigieron vulnerabilidades de inyección Unicode SQL 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 utilizará 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 comillas problemáticas (') que un usuario podría ingresar con un sustituto seguro para MySQL, una comilla de escape \'.

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

// Conéctate 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.

Advertencia obsoleta:El código de muestra de esta respuesta (como el código de muestra de la pregunta) usa PHP MySQL extensión, que quedó obsoleta en PHP 5.5.0 y se eliminó por completo en PHP 7.0.0.

Advertencia de seguridad:Esta respuesta no está en línea con las mejores prácticas de seguridad. Escapar no es adecuado para evitar la inyección de SQL, usar declaraciones preparadas en cambio.Utilice la estrategia que se describe a continuación bajo su propia responsabilidad.(También, mysql_real_escape_string() fue eliminado en PHP 7.)

Podrías hacer algo básico como esto:

$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

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

Independientemente de lo que termines usando, asegúrate de comprobar que tu entrada no haya sido alterada por magic_quotes o alguna otra basura bien intencionada y, si es necesario, revísela stripslashes o lo que sea para desinfectarlo.

Advertencia obsoleta:El código de muestra de esta respuesta (como el código de muestra de la pregunta) usa PHP MySQL extensión, que quedó obsoleta en PHP 5.5.0 y se eliminó por completo en PHP 7.0.0.

Advertencia de seguridad:Esta respuesta no está en línea con las mejores prácticas de seguridad. Escapar no es adecuado para evitar la inyección de SQL, usar declaraciones preparadas en cambio.Utilice la estrategia que se describe a continuación bajo su propia responsabilidad.(También, mysql_real_escape_string() fue eliminado en PHP 7.)

La consulta parametrizada Y la validación de entrada es el camino a seguir.Hay muchos escenarios en los que puede ocurrir la 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 SQL inesperada (cuando escapar no es suficiente)

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

Adoptando el patrón MVC y un marco como pastelPHP o CódigoIgniter Probablemente sea el camino correcto a seguir:Tareas comunes como la creación de consultas seguras a bases de datos se han resuelto e implementado de forma centralizada en dichos marcos.Le ayudan a organizar su aplicación web de forma sensata y le hacen pensar más en cargar y guardar objetos que en construir de forma segura consultas SQL individuales.

yo prefiero procedimientos almacenados (MySQL ha tenido soporte para procedimientos almacenados desde 5.0) desde el punto de vista de la seguridad - las ventajas son -

  1. La mayoría de las bases de datos (incluidas mysql) permiten restringir el acceso de los usuarios a la ejecución de procedimientos almacenados.El control de acceso de seguridad detallado es útil para evitar ataques de escalada de privilegios.Esto evita que las aplicaciones comprometidas puedan ejecutar SQL directamente en la base de datos.
  2. Abstraen la consulta SQL sin procesar de la aplicación, por lo que hay menos información disponible para la aplicación sobre la estructura de la base de datos.Esto dificulta que las personas comprendan la estructura subyacente de la base de datos y diseñen ataques adecuados.
  3. Sólo aceptan parámetros, por lo que las ventajas de las consultas parametrizadas están ahí.Por supuesto, en mi opinión, aún necesita desinfectar su entrada, especialmente si está utilizando SQL dinámico dentro del procedimiento almacenado.

Las desventajas son -

  1. Ellos (los procedimientos almacenados) son difíciles de mantener y tienden a multiplicarse muy rápidamente.Esto hace que gestionarlos sea un problema.
  2. No son muy adecuados para consultas dinámicas: si están diseñados para aceptar código dinámico como parámetros, muchas de las ventajas quedan anuladas.

Hay muchas formas de prevenir inyecciones de SQL y otros ataques de SQL.Puede encontrarlo fácilmente en Internet (Búsqueda de Google).Por supuesto PDO es una de las buenas soluciones. Pero me gustaría sugerirle algunas buenas prevención de enlaces de inyección SQL.

¿Qué es la inyección SQL y cómo prevenirla?

Manual de PHP para inyección SQL

Explicación de Microsoft sobre inyección y prevención de SQL en PHP

y algunos otros como Prevención de la inyección de SQL con MySQL y PHP

Ahora, ¿Por qué necesita evitar que su consulta se inyecte SQL?

Me gustaría informarle:¿Por qué intentamos prevenir la inyección de SQL con un breve ejemplo a continuación?

Consulta de coincidencia de autenticación de inicio de sesión:

$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";

Ahora bien, si alguien (un hacker) pone

$_POST['email']= admin@emali.com' OR '1=1

y contraseña cualquier cosa....

La consulta se analizará en el sistema solo hasta:

$query="select * from users where email='admin@emali.com' OR '1=1';

La otra parte será descartada.Entonces, ¿qué pasará?Un usuario no autorizado (hacker) podrá iniciar sesión como administrador sin tener su contraseña.Ahora, puede hacer cualquier cosa que pueda hacer el administrador o la persona de correo electrónico.Mira, es muy peligroso si no se evita la inyección SQL.

Creo que si alguien quiere usar PHP y MySQL o algún otro servidor de base de datos:

  1. Piensa en aprender DOP (Objetos de datos PHP): es una capa de acceso a la base de datos que proporciona un método uniforme de acceso a múltiples bases de datos.
  2. Piensa en aprender MySQLi
  3. Utilice funciones PHP nativas como: etiquetas_desnudas, mysql_real_escape_string o si es variable numérica, simplemente (int)$foo.Leer más sobre tipos de variables en PHP aquí.Si está utilizando bibliotecas como PDO o MySQLi, utilice siempre DOP::cita() y mysqli_real_escape_string().

Ejemplos de bibliotecas:

---- DOP

----- Sin marcadores de posición: ¡listo para la inyección SQL! Es malo

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");

----- Marcadores de posición sin nombre

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);

----- Marcadores de posición con nombre

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");

--- MySQLi

$request = $mysqliConnection->prepare('
       SELECT * FROM trainers
       WHERE name = ?
       AND email = ?
       AND last_login > ?');

    $query->bind_param('first_param', 'second_param', $mail, time() - 3600);
    $query->execute();

PD:

PDO gana esta batalla con facilidad.Con soporte para doce controladores de bases de datos diferentes y parámetros nombrados, podemos ignorar la pequeña pérdida de rendimiento y acostumbrarnos a su API.Desde el punto de vista de la seguridad, ambos están a salvo siempre que el desarrollador los use de la manera que se supone que deben usarse

Pero aunque tanto PDO como MySQLI son bastante rápidos, MySQLI funciona insignificantemente más rápido en puntos de referencia, ~ 2.5% para declaraciones no preparadas, y ~ 6.5% para las preparadas.

Y pruebe cada consulta a su base de datos; es una mejor manera de evitar la inyección.

Si es posible, emita los tipos de sus parámetros.Pero sólo funciona en tipos simples como int, bool y float.

$unsafe_variable = $_POST['user_id'];

$safe_variable = (int)$unsafe_variable ;

mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

Si desea aprovechar los motores de caché, como Redis o Memcached, tal vez DALMP podría ser una opción.Se utiliza puro MySQLi.Mira esto: Capa de abstracción de base de datos DALMP para MySQL usando PHP.

Además, puede "preparar" sus argumentos antes de preparar su consulta para poder crear consultas dinámicas y al final tener una consulta de declaraciones completamente preparada. Capa de abstracción de base de datos DALMP para MySQL usando PHP.

Para aquellos que no están seguros de cómo utilizar DOP (procedente del mysql_ funciones), hice un Envoltorio PDO muy, muy simple. ese es un solo archivo.Existe para mostrar lo fácil que es hacer todas las cosas comunes que deben realizarse en las aplicaciones.Funciona con PostgreSQL, MySQL y SQLite.

Básicamente, léelo. mientras lees el manual para ver cómo utilizar las funciones PDO en la vida real para simplificar el almacenamiento y la recuperación de valores en el formato desear.

quiero una sola columna

$count = DB::column('SELECT COUNT(*) FROM `user`);

Quiero resultados de una matriz (clave => valor) (es decir,para hacer un cuadro de selección)

$pairs = DB::pairs('SELECT `id`, `username` FROM `user`);

quiero un resultado de una sola fila

$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));

quiero una variedad de resultados

$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array(TRUE));

Usando esta función PHP mysql_escape_string() Puedes conseguir una buena prevención de forma rápida.

Por ejemplo:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."'

mysql_escape_string — Escapa de una cadena para usarla en mysql_query

Para mayor prevención, puedes agregar al final…

wHERE 1=1   or  LIMIT 1

Finalmente obtienes:

SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."' LIMIT 1

Algunas pautas para utilizar caracteres de escape en declaraciones SQL.

no usar mysql, esta extensión está en desuso, use MySQLi o DOP.

MySQLi

Para escapar manualmente de caracteres especiales en una cadena, puede utilizar el mysqli_real_escape_string función.La función no funcionará correctamente a menos que se establezca el juego de caracteres correcto con mysqli_set_charset.

Ejemplo:

$mysqli = new mysqli( 'host', 'user', 'password', 'database' );
$mysqli->set_charset( 'charset');

$string = $mysqli->real_escape_string( $string );
$mysqli->query( "INSERT INTO table (column) VALUES ('$string')" );

Para el escape automático de valores con declaraciones preparadas, utilice mysqli_prepare, y mysqli_stmt_bind_param donde se deben proporcionar los tipos para las variables de vinculación correspondientes para una conversión adecuada:

Ejemplo:

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

$stmt->bind_param( "is", $integer, $string );

$stmt->execute();

No importa si usa declaraciones preparadas o mysqli_real_escape_string, siempre debe saber el tipo de datos de entrada con los que está trabajando.

Entonces, si usa una declaración preparada, debe especificar los tipos de variables para la función mysqli_stmt_bind_param.

Y el uso de mysqli_real_escape_string es, como su nombre lo indica, para escapar de caracteres especiales en una cadena, por lo que no hará que los números enteros sean seguros.El propósito de esta función es evitar la rotura de cadenas en sentencias SQL y el daño a la base de datos que esto podría causar.mysqli_real_escape_string es una función útil cuando se usa correctamente, especialmente cuando se combina con sprintf.

Ejemplo:

$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0';

$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string( $string ), $integer );

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5

$integer = '99999999999999999999';
$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string( $string ), $integer );

echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647

La alternativa sencilla a este problema podría resolverse otorgando los permisos adecuados en la propia base de datos.Por ejemplo:Si está utilizando una base de datos MySQL, ingrese a la base de datos a través de la terminal o la interfaz de usuario proporcionada y simplemente siga este comando:

 GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';

Esto restringirá que el usuario se limite únicamente a las consultas especificadas.Elimine el permiso de eliminación para que los datos nunca se eliminen de la consulta realizada desde la página PHP.Lo segundo que debe hacer es eliminar los privilegios para que MySQL actualice los permisos y las actualizaciones.

FLUSH PRIVILEGES; 

más información sobre enjuagar.

Para ver los privilegios actuales del usuario, ejecute la siguiente consulta.

select * from mysql.user where User='username';

Aprender más acerca de CONCEDER.

Advertencia de seguridad:Esta respuesta no está en línea con las mejores prácticas de seguridad. Escapar no es adecuado para evitar la inyección de SQL, usar declaraciones preparadas en cambio.Utilice la estrategia que se describe a continuación bajo su propia responsabilidad.(También, mysql_real_escape_string() fue eliminado en PHP 7.)

Advertencia obsoleta:La extensión mysql está en desuso en este momento.recomendamos utilizar el extensión DOP

Utilizo tres formas diferentes para evitar que mi aplicación web sea vulnerable a la inyección SQL.

  1. Uso de mysql_real_escape_string(), que es una función predefinida en PHP, y este código agrega barras invertidas a los siguientes caracteres: \x00, \n, \r, \, ', " y \x1a.Pase los valores de entrada como parámetros para minimizar la posibilidad de inyección SQL.
  2. La forma más avanzada es utilizar DOP.

Espero que esto ayude.

Considere la siguiente consulta:

$iId = mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string() no protegerá aquí.Si usa comillas simples (' ') alrededor de sus variables dentro de su consulta, eso es lo que lo protege contra esto.Aquí hay una solución a continuación para esto:

$iId = (int) mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";

Este pregunta tiene algunas buenas respuestas sobre esto.

Sugiero que usar PDO es la mejor opción.

Editar:

mysql_real_escape_string() está en desuso a partir de PHP 5.5.0.Utilice mysqli o PDO.

Una alternativa a mysql_real_escape_string() es

string mysqli_real_escape_string ( mysqli $link , string $escapestr )

Ejemplo:

$iId = $mysqli->real_escape_string("1 OR 1=1");
$mysqli->query("SELECT * FROM table WHERE id = $iId");

En cuanto a muchas respuestas útiles, espero agregar algunos valores a este hilo.La inyección SQL es un ataque que se puede realizar a través de entradas del usuario (entradas que el usuario completa y luego usan dentro de las consultas). Los patrones de inyección SQL tienen una sintaxis de consulta correcta, mientras que podemos llamarlo:malas consultas por malas razones, asumimos que puede haber una mala persona que intenta obtener información secreta (evitando el control de acceso) que afecta los tres principios de seguridad (Confidencialidad, Integridad, Disponibilidad).

Ahora, nuestro punto es prevenir amenazas a la seguridad como los ataques de inyección SQL, la pregunta (Cómo prevenir un ataque de inyección SQL usando PHP), sea más realista, el filtrado de datos o la eliminación de datos de entrada es el caso cuando se utilizan datos de entrada del usuario dentro de dichos ataques. consulta, usar PHP o cualquier otro lenguaje de programación no es el caso, o como recomienda más gente usar tecnología moderna como declaraciones preparadas o cualquier otra herramienta que actualmente admita la prevención de inyección SQL, ¿considera que estas herramientas ya no están disponibles?¿Cómo protege su aplicación?

Mi enfoque contra la inyección SQL es:borrar los datos ingresados ​​por el usuario antes de enviarlos a la base de datos (antes de usarlos dentro de cualquier consulta).

Filtrado de datos para (Convertir datos no seguros en datos seguros)Considere eso DOP y MySQLi no disponible, ¿cómo puede proteger su solicitud?¿Me obligas a usarlos?¿Qué pasa con otros lenguajes además de PHP?Prefiero brindar ideas generales, ya que se pueden usar para límites más amplios, no solo para un lenguaje específico.

  1. Usuario SQL (limitando privilegios de usuario):Las operaciones SQL más comunes son (SELECCIONAR, ACTUALIZAR, INSERTAR), entonces, ¿por qué otorgar el privilegio ACTUALIZAR a un usuario que no lo requiere?Por ejemplo iniciar sesión y buscar páginas Solo están usando SELECT, entonces, ¿por qué usar usuarios de DB en estas páginas con altos privilegios?REGLA:no cree un usuario de base de datos para todos los privilegios, para todas las operaciones SQL, puede crear su esquema como (deluser, selectuser, updateuser) como nombres de usuario para facilitar su uso.

ver Principio de privilegio mínimo

  1. Filtrado de datos:Antes de crear cualquier consulta, la entrada del usuario debe validarse y filtrarse; para los programadores, es importante definir algunas propiedades para cada variable de entrada del usuario:tipo de datos, patrón de datos y longitud de datos.un campo que es un número entre (x e y) debe validarse exactamente usando la regla exacta, para un campo que es una cadena (texto):El patrón es el caso, por ejemplo, el nombre de usuario debe contener solo algunos caracteres, digamos [a-zA-Z0-9_-.] la longitud varía entre (x y n) donde x y n (enteros, x <= n).Regla:Crear filtros exactos y reglas de validación son las mejores prácticas para mí.

  2. Utilice otras herramientas:Aquí, también estaré de acuerdo con usted en que la declaración preparada (consulta parametrizada) y los procedimientos almacenados, las desventajas aquí es que estos métodos requieren habilidades avanzadas que no existen para la mayoría de los usuarios, la idea básica aquí es distinguir entre la consulta SQL y los datos. que se usa internamente, ambos enfoques se pueden usar incluso con datos no seguros, porque los datos ingresados ​​por el usuario aquí no agregan nada a la consulta original, como (cualquiera o x=x).Para obtener más información, lea Hoja de trucos para la prevención de inyección SQL de OWASP.

Ahora, si es un usuario avanzado, comience a usar esta defensa como desee, pero, para los principiantes, si no pueden implementar rápidamente el procedimiento almacenado y preparar la declaración, es mejor filtrar los datos de entrada tanto como puedan.

Finalmente, consideremos que el usuario envía este texto a continuación en lugar de ingresar su nombre de usuario:

[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'

Esta entrada se puede verificar temprano sin ninguna declaración preparada ni procedimientos almacenados, pero para estar seguro, su uso comienza después del filtrado y validación de los datos del usuario.

El último punto es detectar comportamientos inesperados que requieren mayor esfuerzo y complejidad;No se recomienda para aplicaciones web normales.El comportamiento inesperado en la entrada del usuario anterior es SELECT, UNION, IF, SUBSTRING, BENCHMARK, SHA, root. Una vez detectadas estas palabras, puede evitar la entrada.

ACTUALIZACIÓN1:

Un usuario comentó que esta publicación es inútil, ¡vale!Esto es lo que OWASP.ORG proporcionó:

Defensas primarias:

Opción 1:Uso de declaraciones preparadas (consultas parametrizadas)
Opcion 2:Uso de procedimientos almacenados
Opción #3:Escapar de todas las entradas proporcionadas por el usuario

Defensas adicionales:

También hacer cumplir:Privilegios mínimos
Realice también:Validación de entrada de lista blanca

Como sabrás, reclamar un artículo debe estar respaldado por un argumento válido, ¡al menos una referencia!De lo contrario, ¡se considera un ataque y un mal reclamo!

Actualización 2:

Del manual de PHP, PHP:Declaraciones Preparadas - Manual:

Escape e inyección SQL

El servidor escapará automáticamente de las variables vinculadas.El servidor inserta sus valores escapados en los lugares apropiados en la plantilla de declaración antes de la ejecución.Se debe proporcionar una pista al servidor para el tipo de variable unida, para crear una conversión apropiada.Vea la función mysqli_stmt_bind_param () para obtener más información.

El escape automático de los valores dentro del servidor a veces se considera una característica de seguridad para evitar la inyección de SQL.El mismo grado de seguridad se puede lograr con las declaraciones no preparadas si los valores de entrada se escapan correctamente.

Actualización 3:

Creé casos de prueba para saber cómo PDO y MySQLi envían la consulta al servidor MySQL cuando usan una declaración preparada:

DOP:

$user = "''1''"; //Malicious keyword
$sql = 'SELECT * FROM awa_user WHERE userame =:username';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array(':username' => $user));

Registro de consultas:

    189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\''
    189 Quit

MySQLi:

$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) {
$stmt->bind_param("s", $user);
$user = "''1''";
$stmt->execute();

Registro de consultas:

    188 Prepare   SELECT * FROM awa_user WHERE username =?
    188 Execute   SELECT * FROM awa_user WHERE username ='\'\'1\'\''
    188 Quit

Está claro que a los datos también se les escapa una declaración preparada, nada más.

Como también se menciona en la declaración anterior. The automatic escaping of values within the server is sometimes considered a security feature to prevent SQL injection. The same degree of security can be achieved with non-prepared statements, if input values are escaped correctly, por lo tanto, esto prueba que la validación de datos como intval() es una buena idea para valores enteros antes de enviar cualquier consulta; además, evitar datos de usuarios maliciosos antes de enviar la consulta es enfoque correcto y válido.

Consulte esta pregunta para obtener más detalles: PDO envía una consulta sin formato a MySQL mientras que Mysqli envía una consulta preparada, ambos producen el mismo resultado

Referencias:

  1. Hoja de trucos de inyección SQL
  2. Inyección SQL
  3. Seguridad de información
  4. Principios de seguridad
  5. Validación de datos

Una forma sencilla sería utilizar un marco PHP como CódigoIgniter o Laravel que tienen funciones incorporadas como filtrado y registro activo para que no tengas que preocuparte por estos matices.

** Advertencia:el enfoque descrito en esta respuesta solo se aplica a escenarios muy específicos y no es seguro ya que los ataques de inyección SQL no solo dependen de poder inyectar X=Y.**

Si los atacantes están intentando hackear el formulario a través de PHP $_GET variable o con la cadena de consulta de la URL, podrá detectarlos si no son seguros.

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php

Porque 1=1, 2=2, 1=2, 2=1, 1+1=2, etc...son las preguntas comunes a una base de datos SQL de un atacante.Quizás también lo utilicen muchas aplicaciones de piratería.

Pero debe tener cuidado de no reescribir una consulta segura desde su sitio.El código anterior le brinda un consejo para reescribir o redirigir (depende de ti) esa cadena de consulta dinámica específica de piratería en una página que almacenará la información del atacante dirección IP, o INCLUSO SUS COOKIES, historial, navegador o cualquier otra información sensible, para que puedas tratar con ellos más tarde prohibiendo su cuenta o contactando a las autoridades.

Hay tantas respuestas para PHP y MySQL, pero aquí está el código para PHP y Oráculo para prevenir la inyección de SQL y el uso regular de los controladores oci8:

$conn = oci_connect($username, $password, $connection_string);
$stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123');
oci_bind_by_name($stmt, ':xx', $fieldval);
oci_execute($stmt);

Una buena idea es utilizar un 'mapeador relacional de objetos' como Idioma:

$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();

$user->first_name = 'Jamie';
$user->save();

$tweets = ORM::for_table('tweet')
    ->select('tweet.*')
    ->join('user', array(
        'user.id', '=', 'tweet.user_id'
    ))
    ->where_equal('user.username', 'j4mie')
    ->find_many();

foreach ($tweets as $tweet) {
    echo $tweet->text;
}

¡No sólo te salva de inyecciones de SQL sino también de errores de sintaxis!También admite colecciones de modelos con encadenamiento de métodos para filtrar o aplicar acciones a múltiples resultados a la vez y múltiples conexiones.

Advertencia obsoleta:El código de muestra de esta respuesta (como el código de muestra de la pregunta) usa PHP MySQL extensión, que quedó obsoleta en PHP 5.5.0 y se eliminó por completo en PHP 7.0.0.

Advertencia de seguridad:Esta respuesta no está en línea con las mejores prácticas de seguridad. Escapar no es adecuado para evitar la inyección de SQL, usar declaraciones preparadas en cambio.Utilice la estrategia que se describe a continuación bajo su propia responsabilidad.(También, mysql_real_escape_string() fue eliminado en PHP 7.)

Usando DOP y MYSQLi es una buena práctica para evitar inyecciones de SQL, pero si realmente desea trabajar con funciones y consultas de MySQL, sería mejor usar

mysql_real_escape_string

$unsafe_variable = mysql_real_escape_string($_POST['user_input']);

Hay más habilidades para prevenir esto:como identificar: si la entrada es una cadena, un número, un carácter o una matriz, hay muchas funciones incorporadas para detectar esto.Además, sería mejor utilizar estas funciones para comprobar los datos de entrada.

es_cadena

$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');

es_numérico

$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');

Y es mucho mejor usar esas funciones para verificar los datos de entrada con mysql_real_escape_string.

Escribí esta pequeña función hace varios años:

function sqlvprintf($query, $args)
{
    global $DB_LINK;
    $ctr = 0;
    ensureConnection(); // Connect to database if not connected already.
    $values = array();
    foreach ($args as $value)
    {
        if (is_string($value))
        {
            $value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
        }
        else if (is_null($value))
        {
            $value = 'NULL';
        }
        else if (!is_int($value) && !is_float($value))
        {
            die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
        }
        $values[] = $value;
        $ctr++;
    }
    $query = preg_replace_callback(
        '/{(\\d+)}/', 
        function($match) use ($values)
        {
            if (isset($values[$match[1]]))
            {
                return $values[$match[1]];
            }
            else
            {
                return $match[0];
            }
        },
        $query
    );
    return $query;
}

function runEscapedQuery($preparedQuery /*, ...*/)
{
    $params = array_slice(func_get_args(), 1);
    $results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.   
    return $results;
}

Esto permite ejecutar declaraciones en un formato String.Format de C# de una sola línea como:

runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);

Se escapa considerando el tipo de variable.Si intenta parametrizar nombres de tablas y columnas, fallará ya que pone cada cadena entre comillas, lo cual es una sintaxis no válida.

ACTUALIZACIÓN DE SEGURIDAD:El anterior str_replace La versión permitía inyecciones agregando {#} tokens a los datos del usuario.Este preg_replace_callback La versión no causa problemas si el reemplazo contiene estos tokens.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top