¿Cómo puedo evitar la inyección de SQL en PHP?

2773

Si la entrada del usuario se inserta sin modificación en una consulta SQL, 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?

Andrew G. Johnson
fuente

Respuestas:

8961

Use declaraciones preparadas y consultas parametrizadas. Estas son declaraciones SQL que el servidor de la base de datos envía y analiza por separado de cualquier parámetro. De esta manera, es imposible que un atacante inyecte SQL malicioso.

Básicamente tienes dos opciones para lograr esto:

  1. 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
    }
  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 referirse (por ejemplo, pg_prepare()y pg_execute()para PostgreSQL). PDO es la opción universal.


Configurar correctamente la conexión

Tenga en cuenta que cuando se usa PDOpara acceder a una base de datos MySQL, las declaraciones preparadas reales no se usan por defecto . Para solucionar esto, debe deshabilitar 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', '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, el script no se detendrá con un Fatal Errorcuando algo sale mal. Y le da al desarrollador la oportunidad de catchcualquier error que sea thrown como PDOExceptions.

Sin embargo, lo que es obligatorio es la primera setAttribute()línea, que le dice a PDO que deshabilite las declaraciones preparadas emuladas y use real declaraciones preparadas . Esto asegura que la declaración y los valores no sean analizados por PHP antes de enviarlos al servidor MySQL (dando a un posible atacante la posibilidad de inyectar SQL malicioso).

Aunque puede establecer las charsetopciones en el constructor, es importante tener en cuenta que las versiones 'antiguas' de PHP (anteriores a 5.3.6) ignoraron en silencio el parámetro charset en el DSN.


Explicación

El prepareservidor de bases de datos analiza y compila la instrucción SQL a la que pasa . Al especificar parámetros (ya sea uno ?o un parámetro con nombre como :nameen el ejemplo anterior), le dice al motor de la base de datos dónde desea filtrar. Entonces cuando llamasexecute , 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 enviar 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 use una declaración preparada solo se tratará como cadenas (aunque el motor de la base de datos puede hacer alguna optimización, por lo que los parámetros también pueden terminar como números, por supuesto). En el ejemplo anterior, si la $namevariable contiene 'Sarah'; DELETE FROM employeesel resultado, simplemente sería 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 dará algunas ganancias de velocidad.

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

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

$preparedStatement->execute([ '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';
}
PeeHaa
fuente
86
Solo para agregar porque no lo vi en ningún otro lugar aquí, otra línea de defensa es un firewall de aplicaciones web (WAF) que puede tener reglas establecidas para buscar ataques de inyección sql:
jkerak
37
Además, la documentación oficial de mysql_query solo permite ejecutar una consulta, por lo que cualquier otra consulta además; es ignorado Incluso si esto ya está en desuso, hay muchos sistemas bajo PHP 5.5.0 y que pueden usar esta función. php.net/manual/en/function.mysql-query.php
Randall Valenciano
12
Este es un mal hábito, pero es una solución posterior al problema: no solo para la inyección de SQL, sino para cualquier tipo de inyecciones (por ejemplo, hubo un agujero de inyección de plantilla de vista en F3 framework v2) si tiene un sitio web o aplicación antigua que está sufriendo de defectos de inyección, una solución es reasignar los valores de sus variables predefinidas supperglobales como $ _POST con valores escapados en bootstrap. Por PDO, aún es posible escapar (también para los frameworks de hoy): substr ($ pdo-> quote ($ str, \ PDO :: PARAM_STR), 1, -1)
Alix
15
Esta respuesta carece de la explicación de lo que es una declaración preparada, una cosa: es un impacto en el rendimiento si usa muchas declaraciones preparadas durante su solicitud y, a veces, representa 10 veces el impacto en el rendimiento. Un mejor caso sería usar PDO con el enlace de parámetros desactivado, pero la preparación de instrucciones desactivada.
donis
66
Usar PDO es mejor, en caso de que esté usando una consulta directa, asegúrese de usar mysqli :: escape_string
Kassem Itani el
1652

Advertencia desaprobada: el código de muestra de esta respuesta (como el código de muestra de la pregunta) utiliza la MySQLextensión de PHP , que se desaprobó 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 es inadecuado para evitar la inyección de SQL , utilice en su lugar declaraciones preparadas . Use la estrategia descrita a continuación bajo su propio riesgo. (Además, mysql_real_escape_string()se eliminó en PHP 7.)

Si está utilizando una versión reciente de PHP, la mysql_real_escape_stringopción descrita a continuación ya no estará disponible (aunque mysqli::escape_stringes un equivalente moderno). En estos días, la mysql_real_escape_stringopción solo tendría sentido para el código heredado en una versión anterior de PHP.


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

Cubriremos la cuerda de menor impacto que escapa uno 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

Véanse también los detalles de mysql_real_escape_string función.

Para usar la consulta parametrizada, debe usar MySQLi en lugar de las funciones de MySQL . 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 que querrá leer allí sería mysqli::prepare .

Además, como han sugerido otros, puede resultarle útil / más fácil intensificar 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 alterar 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 que pasara la entrada del usuario a través de una lista blanca para asegurarse de que solo se permitan valores "seguros".
  • Si utiliza enteros de la entrada del usuario en una condición y adopta el mysql_real_escape_stringenfoque, sufrirá el problema descrito por Polynomial 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 lidiar al validar que la entrada del usuario contiene solo dígitos.
  • Probablemente hay otros casos que no conozco. Puede encontrar que este es un recurso útil en algunos de los problemas más sutiles que puede encontrar.
Matt Sheppard
fuente
1
usar mysql_real_escape_stringes suficiente o debo usar parametrizado también?
peiman F.
55
@peimanF. mantenga una buena práctica de usar consultas parametrizadas, incluso en un proyecto local. Con las consultas parametrizadas, tiene la garantía de que no habrá inyección de SQL. Pero tenga en cuenta que debe desinfectar los datos para evitar la recuperación falsa (es decir, la inyección de XSS, como poner el código HTML en un texto) con, htmlentitiespor ejemplo
Goufalite el
2
@peimanF. Buena práctica para consultas parametrizadas y valores de enlace, pero la cadena de escape real es buena por ahora
Richard
Entiendo la inclusión de mysql_real_escape_string()para completar, pero no soy fanático de enumerar primero el enfoque más propenso a errores. El lector podría tomar rápidamente el primer ejemplo. Menos mal que ahora está en desuso :)
Steen Schütt
1
@ SteenSchütt: todas las mysql_*funciones están en desuso. Fueron reemplazados por funciones similares mysqli_* , como mysqli_real_escape_string.
Rick James
1073

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

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

Y las declaraciones preparadas cubren solo dos de ellas.

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

En general, este enfoque de protección se basa en la lista blanca .

En este caso, cada parámetro dinámico debe estar codificado en su script y elegido de ese conjunto. Por ejemplo, para hacer un pedido dinámico:

$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 la lista blanca que hace todo el trabajo en una 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 seguir en la lista blanca como un enfoque más sólido y explícito. Sin embargo, siempre que tenga un identificador entre comillas, puede escapar del carácter de comillas para que sea seguro. Por ejemplo, de forma predeterminada para mysql, debe duplicar el carácter de comillas para escapar de él . Para otros otros DBMS, las reglas de escape serían diferentes.

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

Entonces, una recomendación general puede expresarse como

  • Cualquier variable que represente un literal de datos SQL (o, para decirlo simplemente: una cadena SQL o un número) debe agregarse a través de una declaración preparada. Sin excepciones.
  • Cualquier otra parte de 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 hay un acuerdo general sobre las mejores prácticas con respecto a la protección de inyección SQL, 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 , todas eliminadas por la comunidad debido a la mala calidad o 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, hay (1) (2) todavía (3) muchas (4) respuestas (5) , incluida la segunda respuesta más votada sugiere el escape manual de cadenas: un enfoque obsoleto que se ha demostrado que es inseguro.

O hay una respuesta un poco mejor que sugiere otro método de formato de cadena e incluso se jacta de ser la panacea definitiva. Si bien, por supuesto, no lo es. Este método no es mejor que el formato de cadena normal, pero mantiene todos sus inconvenientes: es aplicable solo a cadenas y, como cualquier otro formato 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 protección contra las inyecciones SQL.

Independientemente de lo que el manual de PHP haya dicho durante años, *_escape_stringde ninguna manera hace que los datos sean seguros y nunca ha sido pensado. Además de ser inútil para cualquier parte de SQL que no sea 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 una absoluta 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, cada variable tiene que formatearse adecuadamente para ser puesta en una consulta, sin importar la fuente nuevamente. 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 particular es "segura" o no), él / ella da su 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 característica de citas muy mágicas, ya despreciadas, desaprobadas y eliminadas.

Entonces, a diferencia de cualquier "escape", las declaraciones preparadas son la medida que realmente protege de la inyección SQL (cuando corresponde).

Su sentido común
fuente
848

Recomiendo usar PDO (PHP Data Objects) 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, hace que su aplicación sea un poco más abstracta de la base de datos, en el raro caso de que tenga que cambiar de proveedor de base de datos.

Kibbee
fuente
619

Uso PDOy consultas preparadas.

( $connes un PDOobjeto)

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

Como puede ver, las personas sugieren que utilice declaraciones preparadas como máximo. No está mal, pero cuando su consulta se ejecuta solo una vez por proceso, habrá una ligera penalización de rendimiento.

Estaba enfrentando este problema, pero creo que lo resolví de una manera muy sofisticada, la forma en que los hackers usan para evitar el uso de comillas. Usé esto junto con declaraciones preparadas emuladas. Lo uso para prevenir todo tipo de posibles ataques de inyección SQL.

Mi acercamiento:

  • Si espera que la entrada sea entera, asegúrese de que sea realmente entera. En un lenguaje de tipo variable como PHP, esto es 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 espera algo más de un número hexadecimal hechizo . Si lo hechizas, escaparás perfectamente de toda entrada. En C / C ++ hay una función llamada mysql_hex_string(), en PHP puedes usar bin2hex().

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

  • Este método hexadecimal se usa a menudo cuando transfiere datos binarios, pero no veo ninguna razón por la que no lo use en todos los datos para evitar ataques de inyección SQL. Tenga en cuenta que debe anteponer los datos 0xo utilizar la función MySQL en su UNHEXlugar.

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 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 ** ** 0x prefijo sólo se puede utilizar para las columnas de datos como char, varchar, texto, bloque, binario, etc .
Además, su uso es un poco complicado si está a punto de insertar una cadena vacía. Tendrá que reemplazarlo por completo ''o recibirá un error.

UNHEX () funciona en cualquier columna; No tiene que preocuparse por la cadena vacía.


Los métodos hexadecimales se usan 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 enteros son como cadenas y se escapan solo con mysql_real_escape_string. Entonces puede 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:

SELECT ... WHERE id = -1 union all select table_name from information_schema.tables

y ahora solo extrae la estructura de la tabla:

SELECT ... WHERE id = -1 union all select column_name from information_schema.column donde table_name = 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')

Zaffy
fuente
66
@SumitGupta Sí, lo hiciste. MySQL no concatena con +pero con CONCAT. Y para el rendimiento: no creo que afecte el rendimiento porque mysql tiene que analizar los datos y no importa si el origen es cadena o hexadecimal
Zaffy
11
@YourCommonSense No entiendes el concepto ... Si quieres tener una cadena en mysql, la citas así 'root'o puedes hexadecimal 0x726f6f74PERO si quieres un número y enviarlo como una cadena probablemente escribirás '42' no CHAR (42 ) ... '42' en hexadecimal sería 0x3432no0x42
Zaffy
77
@YourCommonSense No tengo nada que decir ... solo jajaja ... si todavía quieres probar hexadecimal en campos numéricos, mira el segundo comentario. Apuesto a que funcionará.
Zaffy 01 de
2
La respuesta indica claramente que no funcionará de esa manera con valores enteros, la razón es que bin2hex convierte el valor pasado en una cadena (y, por lo tanto, bin2hex (0) es 0x30 y no 0x03), esa es probablemente la parte que lo confunde . Si sigue eso, funciona perfectamente (al menos en mi sitio, probado con 4 versiones diferentes de mysql en máquinas debian, 5.1.x a 5.6.x). Después de todo, hexadecimal es solo la forma de representación, no el valor;)
griffin
55
@YourCommonSense que todavía no entiendes? No puede usar 0x y concat porque si la cadena está vacía, terminará con un error. Si desea una alternativa simple a su consulta, pruebe estaSELECT title FROM article WHERE id = UNHEX(' . bin2hex($_GET["id"]) . ')
Zaffy
499

Advertencia desaprobada: el código de muestra de esta respuesta (como el código de muestra de la pregunta) utiliza la MySQLextensión de PHP , que se desaprobó 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 es inadecuado para evitar la inyección de SQL , utilice en su lugar declaraciones preparadas . Use la estrategia descrita a continuación bajo su propio riesgo. (Además, mysql_real_escape_string()se eliminó en PHP 7.)

IMPORTANTE

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

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 para detener la inyección de SQL , consulte esta mysql_real_escape_string()omisión y las vulnerabilidades de inyección de Unicode SQL recientemente reparadas en WordPress .

Prevención de inyección - mysql_real_escape_string ()

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

mysql_real_escape_stringtoma una cadena que se va a 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 comillas problemáticas (') que un usuario podría ingresar con un sustituto seguro de MySQL, una comilla escapada'.

NOTA: ¡debe estar conectado a la base de datos para usar 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;

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

rahularyansharma
fuente
30
Esto es lo mejor que puede hacer con la extensión heredada de mysql. Para el nuevo código, se recomienda cambiar a mysqli o PDO.
Álvaro González
77
No estoy de acuerdo con esta "una función especialmente diseñada para prevenir estos ataques". Creo que ese mysql_real_escape_stringpropósito es permitir construir una consulta SQL correcta para cada cadena de datos de entrada. La inyección sql de prevención es el efecto secundario de esta función.
sectus
44
no utiliza funciones para escribir cadenas de datos de entrada correctas. Simplemente escribe los correctos que no necesitan escapar o que ya se han escapado. mysql_real_escape_string () puede haber sido diseñado con el propósito que usted menciona en mente, pero su único valor es prevenir la inyección.
Nazca
17
¡ADVERTENCIA! mysql_real_escape_string() No es infalible .
eggyal
99
mysql_real_escape_stringahora está en desuso, por lo que ya no es una opción viable. Será eliminado en el futuro de PHP. Es mejor pasar a lo que recomiendan las personas de PHP o MySQL.
jww
462

Podrías hacer algo básico como esto:

$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query($dbConnection, "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, el formato de la variable (números, letras, etc.).

Kiran Maniya
fuente
28
He probado tu ejemplo y funciona bien para mí. ¿Podrías aclarar "esto no resolverá todos los problemas"
Chinook
14
Si no cita la secuencia, todavía es inyectable. Toma $q = "SELECT col FROM tbl WHERE x = $safe_var";por ejemplo. Establecer $safe_varen 1 UNION SELECT password FROM userstrabajos en este caso debido a la falta de comillas. También es posible inyectar cadenas en la consulta usando CONCATy CHR.
Polinómico el
44
@Polynomial Completamente correcto, pero vería esto simplemente como un uso incorrecto. Mientras lo use correctamente, definitivamente funcionará.
glglgl
22
¡ADVERTENCIA! mysql_real_escape_string() No es infalible .
eggyal
8
mysql_real_escape_stringahora está en desuso, por lo que ya no es una opción viable. Será eliminado en el futuro de PHP. Es mejor pasar a lo que recomiendan las personas de PHP o MySQL.
jww
380

Lo que sea que termines usando, asegúrate de verificar que tu entrada no haya sido destrozada por magic_quotesalguna otra basura bien intencionada, y si es necesario, revísalo stripslasheso lo que sea para desinfectarlo.

Rob
fuente
11
En efecto; correr con magic_quotes activado solo fomenta la mala práctica. Sin embargo, a veces no siempre puede controlar el entorno a ese nivel, o no tiene acceso para administrar el servidor o su aplicación tiene que coexistir con aplicaciones que (estremecerse) dependen de dicha configuración. Por estas razones, es bueno escribir aplicaciones portátiles, aunque obviamente el esfuerzo se desperdicia si controla el entorno de implementación, por ejemplo, porque es una aplicación interna o solo se utilizará en su entorno específico.
Rob
24
A partir de PHP 5.4, la abominación conocida como 'comillas mágicas' ha sido asesinada a muerte . Y buen viaje a la basura mala.
BryanH
363

Advertencia desaprobada: el código de muestra de esta respuesta (como el código de muestra de la pregunta) utiliza la MySQLextensión de PHP , que se desaprobó 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 es inadecuado para evitar la inyección de SQL , utilice en su lugar declaraciones preparadas . Use la estrategia descrita a continuación bajo su propio riesgo. (Además, mysql_real_escape_string()se eliminó en PHP 7.)

La consulta parametrizada Y la validación de entrada es el camino a seguir. Hay muchos escenarios bajo los cuales puede ocurrir la inyección SQL, aunque mysql_real_escape_string()se haya utilizado.

Esos ejemplos son vulnerables a la inyección de 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 puede usar 'para proteger la encapsulación.

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

Cedric
fuente
2
Puede evitar la inyección de SQL si adopta una técnica de validación de entrada en la que la entrada del usuario se autentica con un conjunto de reglas definidas para longitud, tipo y sintaxis y también con reglas comerciales.
Josip Ivic
312

En mi opinión, la mejor manera de evitar generalmente la inyección de SQL en su aplicación PHP (o en cualquier aplicación web) 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 hable con la base de datos, lo está haciendo mal. De esa manera, es solo cuestión de tiempo hasta que olvides formatear correctamente tu consulta en algún punto de tu código.

Adoptar el patrón MVC y un marco como CakePHP o CodeIgniter es probablemente el camino correcto: las tareas comunes como la creación de consultas seguras en la base de datos se han resuelto e implementado centralmente en dichos marcos. Le ayudan a organizar su aplicación web de una manera sensata y le hacen pensar más en cargar y guardar objetos que en construir de forma segura consultas SQL únicas.

Johannes Fahrenkrug
fuente
55
Creo que tu primer párrafo es importante. La comprensión es la clave. Además, no todos trabajan para una empresa. Para una gran franja de personas, los marcos realmente van en contra de la idea de comprensión . Intimar con los fundamentos puede no ser valorado mientras se trabaja dentro de una fecha límite, pero los aficionados al bricolaje disfrutan ensuciarse las manos. Los desarrolladores de framework no son tan privilegiados que todos los demás deben inclinarse y asumir que nunca cometerán errores. El poder de tomar decisiones sigue siendo importante. ¿Quién puede decir que mi marco no desplazará a otro esquema en el futuro?
Anthony Rutledge
@AnthonyRutledge Tienes toda la razón. Es muy importante entender lo que está sucediendo y por qué. Sin embargo, la posibilidad de que un marco verdadero y probado y utilizado y desarrollado activamente se haya encontrado y resuelto muchos problemas y haya solucionado muchos agujeros de seguridad ya es bastante alto. Es una buena idea mirar la fuente para tener una idea de la calidad del código. Si es un desastre no probado, probablemente no sea seguro.
Johannes Fahrenkrug
3
Aquí. Aquí. Buenos puntos. Sin embargo, estaría de acuerdo en que muchas personas pueden estudiar y aprender a adoptar un sistema MVC, pero no todos pueden reproducirlo a mano (controladores y servidor). Uno puede ir demasiado lejos con este punto. ¿Necesito entender mi microondas antes de calentar mis galletas de nuez con mantequilla de maní que me hizo mi amiga? ;-)
Anthony Rutledge
3
@AnthonyRutledge Estoy de acuerdo! Creo que el caso de uso también marca la diferencia: ¿estoy creando una galería de fotos para mi página de inicio personal o estoy creando una aplicación web de banca en línea? En este último caso, es muy importante comprender los detalles de seguridad y cómo un marco que estoy usando los aborda.
Johannes Fahrenkrug
3
Ah, la excepción de seguridad al corolario hágalo usted mismo. Mira, tiendo a estar dispuesto a arriesgarlo todo e ir a la quiebra. :-) Broma. Con suficiente tiempo, las personas pueden aprender a hacer una aplicación bastante segura. Demasiada gente tiene prisa. Levantan las manos y asumen que los marcos son más seguros . Después de todo, no tienen tiempo suficiente para probar y resolver las cosas. Además, la seguridad es un campo que requiere un estudio dedicado. No es algo que los simples programadores conozcan en profundidad en virtud de comprender algoritmos y patrones de diseño.
Anthony Rutledge
298

Estoy a favor de los procedimientos almacenados ( MySQL ha tenido soporte de procedimientos almacenados desde 5.0 ) desde un punto de vista de seguridad, las ventajas son:

  1. La mayoría de las bases de datos (incluido MySQL ) permiten restringir el acceso del usuario 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 contra la base de datos.
  2. Extraen la consulta SQL sin procesar de la aplicación para que haya menos información de la estructura de la base de datos disponible para la aplicación. Esto dificulta que las personas comprendan la estructura subyacente de la base de datos y diseñen ataques adecuados.
  3. Solo 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 (procedimientos almacenados) son difíciles de mantener y tienden a multiplicarse muy rápidamente. Esto hace que administrarlos sea un problema.
  2. No son muy adecuados para consultas dinámicas: si están diseñados para aceptar códigos dinámicos como parámetros, se niegan muchas de las ventajas.
Nikhil
fuente
297

Hay muchas formas de prevenir las inyecciones SQL y otros hacks 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 algunos buenos enlaces de prevención de la inyección SQL.

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

Manual PHP para inyección SQL

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

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

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

Me gustaría hacerle saber: ¿Por qué tratamos de prevenir la inyección de SQL con un breve ejemplo a continuación:

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

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

Ahora, 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='[email protected]' 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, él / ella puede hacer cualquier cosa que el administrador / persona de correo electrónico pueda hacer. Mira, es muy peligroso si no se previene la inyección SQL.

Manish Shrivastava
fuente
267

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

  1. Piense en aprender PDO (PHP Data Objects): 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: strip_tags , mysql_real_escape_string o, si es una variable numérica, solo (int)$foo. Lea más sobre el tipo de variables en PHP aquí . Si está utilizando bibliotecas como PDO o MySQLi, use siempre PDO :: quote () y mysqli_real_escape_string () .

Ejemplos de bibliotecas:

---- PDO

----- Sin marcadores de posición: ¡listo para la inyección de 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 nombrados

$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 con nombre, podemos ignorar la pequeña pérdida de rendimiento y acostumbrarnos a su API. Desde el punto de vista de la seguridad, ambos son seguros siempre que el desarrollador los use de la forma en que se supone que deben usarse.

Pero aunque tanto PDO como MySQLi son bastante rápidos, MySQLi se desempeña de manera insignificantemente más rápida en los puntos de referencia: ~ 2.5% para las declaraciones no preparadas y ~ 6.5% para las preparadas.

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

RDK
fuente
que mysqli es incorrecto El primer parámetro expresa los tipos de datos.
mickmackusa
257

Si es posible, eche los tipos de sus parámetros. Pero solo 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 . "')");
devOp
fuente
3
Este es uno de los pocos casos en los que usaría un "valor escapado" en lugar de una declaración preparada. Y la conversión de tipo entero es extremadamente eficiente.
HoldOffHunger
224

Para aquellos que no están seguros de cómo usar PDO (proveniente de las mysql_funciones), hice un contenedor PDO muy, muy simple que es un solo archivo. Existe para mostrar lo fácil que es hacer todas las cosas comunes que deben hacerse las aplicaciones. Funciona con PostgreSQL, MySQL y SQLite.

Básicamente, léalo mientras lee el manual para ver cómo usar las funciones PDO en la vida real para que sea más fácil almacenar y recuperar valores en el formato que desee.

Quiero una sola columna

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

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

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

Quiero un solo resultado de fila

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

Quiero una serie de resultados.

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

Usando esta función PHP mysql_escape_string()puede obtener una buena prevención de una manera rápida.

Por ejemplo:

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

mysql_escape_string - Se escapa de una cadena para usar en un mysql_query

Para más prevención, puede 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
Nicolas Finelli
fuente
220

Algunas pautas para escapar caracteres especiales en sentencias SQL.

No uses MySQL . Esta extensión está en desuso. Utilice MySQLi o PDO en su lugar.

MySQLi

Para escapar manualmente caracteres especiales en una cadena, puede usar la función mysqli_real_escape_string . La función no funcionará correctamente a menos que el conjunto de caracteres correcto esté configurado 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, use mysqli_prepare y mysqli_stmt_bind_param donde se deben proporcionar los tipos para las variables de enlace 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 mysqli_stmt_bind_param función.

Y el uso de mysqli_real_escape_stringes para, como su nombre lo indica, escapar caracteres especiales en una cadena, por lo que no hará que los enteros sean seguros. El propósito de esta función es evitar romper las cadenas en las declaraciones SQL y el daño a la base de datos que podría causar.mysqli_real_escape_stringes 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
Danijel
fuente
3
La pregunta es muy genérica. Algunas excelentes respuestas anteriores, pero la mayoría sugieren declaraciones preparadas. MySQLi async no admite declaraciones preparadas, por lo que el sprintf parece una excelente opción para esta situación.
Dustin Graham
183

La alternativa simple a este problema podría resolverse otorgando los permisos apropiados en la base de datos. Por ejemplo: si está utilizando una base de datos MySQL, ingrese a la base de datos a través del 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 solo a la consulta especificada. Elimine el permiso de eliminación para que los datos nunca se eliminen de la consulta activada 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 flush .

Para ver los privilegios actuales para el usuario, active la siguiente consulta.

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

Obtenga más información sobre GRANT .

Apurv Nerlekar
fuente
25
Esta respuesta es esencialmente incorrecta , ya que no ayuda a prevenir una prevención de inyección sino que trata de suavizar las consecuencias. En vano.
Su sentido común
1
Correcto, no proporciona una solución, pero es lo que puede hacer de antemano para evitar cosas.
Apurv Nerlekar
1
@Apurv Si mi objetivo es leer información privada de su base de datos, no tener el permiso ELIMINAR no significa nada.
Alex Holsgrove
1
@ AlexHolsgrove: Tómelo con calma, solo estaba sugiriendo buenas prácticas para suavizar las consecuencias.
Apurv Nerlekar
2
@Apurv No quiere "suavizar las consecuencias", quiere hacer todo lo posible para protegerse contra él. Sin embargo, para ser justos, es importante establecer el acceso correcto del usuario, pero no es realmente lo que pide el OP.
Alex Holsgrove
177

Con respecto a muchas respuestas útiles, espero agregar algo de valor a este hilo.

La inyección SQL es un ataque que se puede realizar a través de entradas de usuario (entradas que rellena un usuario y luego se utilizan dentro de las consultas). Los patrones de inyección SQL son una sintaxis de consulta correcta, mientras que podemos llamarla: consultas malas por razones malas, y suponemos que podría haber una persona mala que intenta obtener información secreta (sin pasar por el control de acceso) que afecta los tres principios de seguridad (confidencialidad , integridad y disponibilidad).

Ahora, nuestro objetivo es evitar las amenazas de seguridad, como los ataques de inyección SQL, la pregunta que se hace (cómo evitar un ataque de inyección SQL usando PHP), ser más realista, el filtrado de datos o la eliminación de datos de entrada es el caso cuando se usan datos de entrada del usuario dentro tal consulta, el uso de PHP o cualquier otro lenguaje de programación no es el caso, o según lo recomendado por más personas para usar tecnología moderna como la declaración preparada o cualquier otra herramienta que actualmente soporte 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 de entrada del usuario antes de enviarlos a la base de datos (antes de usarlos dentro de cualquier consulta).

Filtrado de datos para (convertir datos inseguros en datos seguros)

Tenga en cuenta que PDO y MySQLi no están disponibles. ¿Cómo puede asegurar su aplicación? ¿Me obligas a usarlos? ¿Qué pasa con otros lenguajes que no sean PHP? Prefiero proporcionar ideas generales, ya que puede usarse para un borde más amplio, no solo para un idioma específico.

  1. Usuario SQL (privilegio de usuario limitado): 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, las páginas de inicio de sesión y búsqueda solo usan 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 mínimo privilegio .

  1. Filtrado de datos: antes de generar cualquier consulta 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, y para un campo que es una cadena (texto): el caso es el patrón, por ejemplo, un nombre de usuario debe contener solo algunos caracteres, vamos a diga [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. Use otras herramientas: Aquí, también estaré de acuerdo con usted en que una declaración preparada (consulta parametrizada) y procedimientos almacenados. Las desventajas aquí son que estas formas 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 utilizan en su interior. Ambos enfoques se pueden usar incluso con datos inseguros, porque los datos ingresados ​​por el usuario aquí no agregan nada a la consulta original, como (any o x = x).

Para obtener más información, lea la hoja de trucos de 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 un procedimiento almacenado y prepararon la declaración, es mejor filtrar los datos de entrada tanto como puedan.

Finalmente, consideremos que un 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 puede verificarse temprano sin ninguna declaración preparada y procedimientos almacenados, pero para estar seguros, su uso comienza después del filtrado y validación de los datos del usuario.

El último punto es detectar comportamientos inesperados que requieren más 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 y root. Una vez que se detectan estas palabras, puede evitar la entrada.

ACTUALIZACIÓN 1:

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

Defensas primarias:

Opción # 1: El uso de sentencias preparadas (consultas parametrizadas)
Opción # 2: El uso de procedimientos almacenados
Opción # 3: Escapar toda la entrada proporcionada por el usuario

defensas adicionales:

También Hacer cumplir: Privilegio
también llevan a cabo: Blanco la lista de entradas de validación

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

Actualización 2:

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

Inyección de escape y SQL

El servidor escapará automáticamente de las variables enlazadas. 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 enlazada, para crear una conversión adecuada. Consulte la función mysqli_stmt_bind_param () para obtener más información.

El escape automático de valores dentro del servidor a veces se considera una característica de seguridad para evitar la inyección de SQL. Se puede lograr el mismo grado de seguridad con 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 se usa 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 una declaración preparada también está escapando de los datos, nada más.

Como también se menciona en la declaración anterior,

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

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, prevenir los datos maliciosos del usuario antes de enviar la consulta es un enfoque correcto y válido .

Consulte esta pregunta para obtener más detalles: PDO envía consultas sin formato a MySQL mientras Mysqli envía consultas preparadas, ambas 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
usuario1646111
fuente
175

Advertencia de seguridad : esta respuesta no está en línea con las mejores prácticas de seguridad. Escapar es inadecuado para evitar la inyección de SQL , utilice en su lugar declaraciones preparadas . Use la estrategia descrita a continuación bajo su propio riesgo. (Además, mysql_real_escape_string()se eliminó en PHP 7.)

Advertencia obsoleta : la extensión mysql está obsoleta en este momento. Recomendamos utilizar la extensión PDO

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

  1. El uso de mysql_real_escape_string(), que es una función predefinida en PHP , y este código add 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 usar PDO.

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";

Esta pregunta tiene algunas buenas respuestas sobre esto.

Sugiero, usar PDO es la mejor opción.

Editar:

mysql_real_escape_string()está en desuso a partir de PHP 5.5.0. Use 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");
Soumalya Banerjee
fuente
169

Una manera simple sería utilizar un marco PHP como CodeIgniter o Laravel que tienen características incorporadas como filtrado y registro activo para que no tenga que preocuparse por estos matices.

Deepak Thomas
fuente
77
Creo que el punto central de la pregunta es hacer esto sin usar dicho marco.
Sanke
147

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 la capacidad de inyección X=Y.

Si los atacantes intentan hackear el formulario a través de la $_GETvariable de PHP o con la cadena de consulta de la URL, podrá atraparlos 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 usan muchas aplicaciones de hackeo.

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

5ervant
fuente
¿Qué pasa con 1-1=0? :)
Rápli András
@ RápliAndrás Algún tipo de ([0-9\-]+)=([0-9]+).
5vier
127

Hay muchas respuestas para PHP y MySQL , pero aquí hay un código para PHP y Oracle para evitar la inyección de SQL, así como el uso regular de 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);
Chintan Gor
fuente
Por favor, explique los parámetros oci_bind_by_name.
Jahanzeb Awan
127

Una buena idea es usar un mapeador relacional de objetos como Idiorm :

$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 solo lo salva de las inyecciones de SQL, sino también de los 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.

Thomas Ahle
fuente
124

Advertencia obsoleta: el código de muestra de esta respuesta (como el código de muestra de la pregunta) utiliza PHPMySQLextensión , que se desaprobó 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 es inadecuado para evitar la inyección de SQL , utilice en su lugar declaraciones preparadas . Use la estrategia descrita a continuación bajo su propio riesgo. (Además, mysql_real_escape_string()se eliminó en PHP 7.)

Usando PDO y MYSQLi es una buena práctica para evitar inyecciones de SQL, pero si realmente desea trabajar con las 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 evitar 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 usar estas funciones para verificar los datos de entrada.

is_string

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

is_numeric

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

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

Rakesh Sharma
fuente
10
Además, no tiene ningún sentido verificar los miembros de la matriz $ _POST con is_string ()
Su sentido común el
21
¡ADVERTENCIA! mysql_real_escape_string() No es infalible .
eggyal
10
mysql_real_escape_stringahora está en desuso, por lo que ya no es una opción viable. Será eliminado de PHP en el futuro. Es mejor pasar a lo que recomiendan las personas de PHP o MySQL.
jww
2
Tema: No confíe en los datos enviados por el usuario. Cualquier cosa que espere son datos basura con caracteres especiales o lógica booleana, que deberían convertirse en parte de la consulta SQL que puede estar ejecutando. Mantenga los valores de $ _POST solo como datos, no como parte de SQL.
Bimal Poudel
88

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 sentencias en una cadena de un solo trazo C # -ish. Formato como:

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

Se escapa teniendo en cuenta el tipo de variable. Si intenta parametrizar los nombres de tabla y columna, fallará ya que pone cada cadena entre comillas, lo que es una sintaxis no válida.

ACTUALIZACIÓN DE SEGURIDAD: la str_replaceversión anterior permitía inyecciones agregando {#} tokens en los datos del usuario. Esta preg_replace_callbackversión no causa problemas si el reemplazo contiene estos tokens.

Calmarius
fuente