222

Las sentencias dinámicas son sentencias SQL que se crean como cadenas de texto (strings) y en las que se insertan/concatenan valores obtenidos de alguna fuente (normalmente proveniente del usuario), lo que puede hacer que sean vulnerables a inyección SQL si no se sanean las entradas, como por ejemplo:

$id_usuario = $_POST["id"];

mysql_query("SELECT * FROM usuarios WHERE id = $id_usuario");

Eso es un ejemplo de una vulnerabilidad grave en la seguridad de una aplicación (web o no) porque si el usuario introdujese un valor como 1; DROP TABLE usuarios;-- nos encontraríamos con que la sentencia ejecutada sería:

SELECT * FROM usuarios WHERE id = 1; DROP TABLE usuarios;--

Y se eliminaría la tabla Usuarios con todos los datos contenidos en ella.

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

A. Cedano
  • 86,578
  • 19
  • 122
  • 221
Alvaro Montoro
  • 48,157
  • 26
  • 100
  • 179
  • 7
    Creo que lo más sencillo es prevenir el uso de caracteres indebidos así mismo cuanto más seguridad de le añada mejor será nunca hay que conformarse ni limitarse la seguridad que se prevee es inferior a la vulnerabilidad. – Vicente Nov 06 '18 at 12:25

5 Answers5

208

NO USES SENTENCIAS DINÁMICAS NI FUNCIONES mysql_*

Las funciones mysql_* (mysql_connect, mysql_query, etc.) son inseguras por naturaleza y su uso no sólo no está recomendado, sino que se consideran obsoletas y se han eliminado completamente a partir de PHP7.

Incluso los métodos nativos que existen en PHP para sanear las entradas de usuario (como mysql_real_escape_string) pueden presentar (raros) problemas y fallar en algunos casos como cuando se usan codificación de caracteres diferentes a UTF-8 junto a versiones no actualizadas de MySQL (en las páginas de PHP para estas funciones se avisa de este riesgo).


Usa sentencias preparadas y consultas parametrizadas

Aunque se podrían sanear las entradas usando métodos como mysqli_real_escape_string, es más recomendable la utilización de sentencias preparadas o parametrizadas. Las sentencias preparadas te permitirán ejecutar la misma sentencia con gran eficiencia.

En PHP, tienes dos alternativas principales: PDO y MySQLi. Hay varias diferencias entre ambas, pero la principal es que PDO se puede usar con diferentes tipos de base de datos (dependiendo del driver utilizado) mientras que MySQLi es exclusivamente para bases de datos MySQL. Es por ello que recomendaría PDO sobre MySQLi.

PDO

Los marcadores de posición (que indican dónde se sustituirá una cadena por su valor), se pueden definir bien usando un signo de interrogación (?) o bien usando un nombre (generalmente empezando con :). Personalmente prefiero usar un nombre, porque eso me ayuda a encontrar posibles errores en caso de tener múltiples variables.

Aquí dejo un ejemplo para el código de la pregunta:

// la variable $pdo contendrá el objeto con la conexión PDO
$pdo = new PDO('mysql:host=mihost;dbname=basedatos', "usuario", "contraseña");

$id_usuario = $_POST["id"];

$sentencia = $pdo->prepare("SELECT * FROM usuarios WHERE id = :idusuario");
$sentencia=$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$sentencia->bindParam(":idusuario", $id_usuario, PDO::PARAM_INT);
$sentencia->execute();

En este caso, :idusuario se sustituirá por el valor de $_POST["id"] de forma segura, y cuando hace el bind se indica que la variable es de tipo entero (PDO::PARAM_INT).

Nota: si la variable es una cadena de texto se usará PDO::PARAM_STR y no hace falta poner las comillas en la sentencia SQL; al especificarle a PHP que es una cadena, las añadirá automáticamente al hacer el bind.

En caso de que existan varias variables a incluir en la sentencia SQL, se debe incluir un único parámetro para cada uno de los valores que se usan en la sentencia. Del ejemplo anterior, el :idusuario puede usarse una única vez en la consulta que se esta preparando. Si fuera necesario usar el "idusuario" de nuevo en la consulta, se debe crear otro parámetro con el valor de $id_usuario.

$pdo = new PDO('mysql:host=mihost;dbname=basedatos', "usuario", "contraseña");

$id_usuario = $_POST["id"];

$sentencia = $pdo->prepare("UPDATE usuarios SET id = :idusuario WHERE id = :idusuario1");
$sentencia=$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$sentencia->bindParam(":idusuario", $id_usuario, PDO::PARAM_INT);
$sentencia->bindParam(":idusuario1", $id_usuario, PDO::PARAM_INT);
$sentencia->execute();

MySQLi

Este método tiene dos interfaces: una procedural y otra orientada a objetos. La interfaz procedural es muy parecida a mysql_*, y por ello la gente que migra desde mysql_* puede sentirse atraída por la facilidad que mysqli_* ofrece. Aunque, de nuevo personalmente, optaría por la versión POO.

Nota: aunque las funciones mysqli_* suelen ser parecidas a las mysql_*, en algunos casos pueden tener diferentes parámetros de entrada o diferentes salidas, lo que puede llevar a algo de confusión al principio.

El ejemplo de la pregunta quedaría así con MySQLi en su interfaz orientada a objetos:

// en $mysqli tendremos la conexión MySQLi
$mysqli = new mysqli("mihost", "usuario", "contraseña", "basedatos");

$id_usuario = $_POST["id"];

$sentencia = $mysqli->prepare("SELECT * FROM usuarios WHERE id = ?");
$sentencia->bind_param("i", $id_usuario );
$sentencia->execute();

Como se puede ver, es bastante parecido a PDO (cambia un poco cómo se especifica el tipo de valor, i para enteros y s para cadenas, pero la idea es similar).

En la versión procedural de MySQLi, el código equivalente sería:

// en $conn tendríamos la conexión a la base de datos con MySQLi
$conn = mysqli_connect("mihost", "usuario", "contraseña", "basedatos");

$id_usuario = $_POST["id"];

$sentencia = mysqli_prepare("SELECT * FROM usuarios WHERE id = ?");
mysqli_stmt_bind_param($sentencia, "i", $id_usuario);
mysqli_stmt_execute($sentencia);

Fuente y bibliografía para más información en español:

Kenny Barrera
  • 2,075
  • 3
  • 22
  • 38
Alvaro Montoro
  • 48,157
  • 26
  • 100
  • 179
  • 7
    En el bind de PDO, hay alguna ventaja de seguridad al usar PDO::PARAM_INT o PDO::PARAM_STR? ya que hace poco aprendí lo básico de PDO pero no había necesitado usar ese tercer parámetro al preparar la sentencia. – Roberto Sepúlveda Bravo Oct 15 '16 at 04:18
  • 12
    Si no me equivoco, no especificar el tipo no afecta a la seguridad sino al rendimiento. PDO por defecto convertirá el dato a cadena (aunque esto puede depender de la configuración) y luego la base de datos tendrá que convertir de nuevo al tipo de dato de la columna. Seguiría siendo seguro, pero no eficiente. – Alvaro Montoro Oct 15 '16 at 12:25
  • 3
    @AlvaroMontoro pudieras auxiliarme con una duda que tengo?, es decir pretendo plantear una pregunta sobre como iniciar con el usod e un framework escrito en JS y autoresponderme indicando todo el proceso, es correcto como pretendo hacerlo? –  Mar 18 '18 at 21:10
  • 3
    S@ShadowPaz Sí sería correcto; las preguntas con auto-respuesta se permiten y se incentivan. Todo lo que se ajuste a la temática y formato del sitio, y que sirva para mejorar el conocimiento de la comunidad, es bienvenido. – Alvaro Montoro Mar 18 '18 at 22:26
86

Atención: La respuesta aceptada es sin duda buena, pero es insuficiente. Es cierto que el uso de consultas preparadas nos evitan casi los ataques conocidos como inyección SQL de primer grado. Pero si PDO está mal configurado las consultas preparadas podrían ser vulnerables, como se muestra con ejemplos en la respuesta. Al final he puesto los enlaces que han servido de base a la elaboración de esta respuesta, sobre todo Preventing SQL Injection in PHP Applications - the Easy and Definitive Guide y An SQL injection against which prepared statements won't help.

Mi único mérito ha sido la traducción, quizá mejorable en algunos puntos. Nada es 100% seguro, lo sabemos, pero el simple uso de PDO o MySQLi han de ser combinados con otras estrategias, explicadas en el punto 5, para asegurar algo de tanto valor como nuestros datos.

1. Nota introductoria

Inyección de SQL es una técnica para tomar el control de una consulta de base de datos que a menudo resulta en un compromiso de confidencialidad. En algunos casos (por ejemplo, si SELECT 'código diabólico aquí' INTO OUTFILE '/var/www/reverse_shell.php' tiene éxito) esto puede resultar en una toma de posesión completa del servidor.

Dado que la inyección de código (que abarca las técnicas de SQL, LDAP, Command OS y XPath Injection) ha permanecido constantemente en la parte superior de las vulnerabilidades de Top 10 de OWASP, es un tema popular para los bloggers que intentan mojarse en el campo de seguridad de aplicaciones.

Desafortunadamente muchos de los consejos que circulan en Internet (especialmente en los blogs antiguos que ocupan un lugar destacado en los motores de búsqueda) están desactualizados, son involuntariamente engañosos y a menudo peligrosos.

2. Un error muy común: PDO mal configurado

Si usted es un desarrollador PHP que busca obtener el máximo provecho de PDO, le recomendamos que cambie dos de los valores predeterminados:

  1. Desactivar preparaciones emuladas. Esto asegura que obtenga declaraciones preparadas.
  2. Establecer el modo de error para lanzar excepciones. Esto evita que tenga que estar observando los resultados de PDOStatement::execute() y hace que su código sea menos redundante.

Para hacer ambas cosas:

$pdo = new PDO(/* Fill in the blank */);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Debido a que PDO::ATTR_EMULATE_PREPARES está seteado a false , estamos obteniendo instrucciones reales preparadas, y porque hemos seteado PDO::ATTR_ERRMORE a PDO::ERRMODE_EXCEPTION , en lugar de esto ...

$stmt = $pdo->prepare("SELECT * FROM foo WHERE first_name = ? AND last_name = ?");
if ($stmt->execute([$_GET['first_name'], $_GET['last_name'])) {
    $users = $stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
    // Handle error here.
}
$args = [
    json_encode($_GET)
    (new DateTime())->format('Y-m-d H:i:s')
];
$insert = $pdo->prepare("INSERT INTO foo_log (params, time) VALUES (?, ?);");
if (!$insert->execute($args)) {
    // Handle error here.
}

... usted puede simplemente escribir su código así:

try {
    $stmt = $pdo->prepare("SELECT * FROM foo WHERE first_name = ? AND last_name = ?");
    $stmt->execute([$_GET['first_name'], $_GET['last_name']);
    $users = $stmt->fetchAll(PDO::FETCH_ASSOC);
    $args = [
        json_encode($_GET),
        (new DateTime())->format('Y-m-d H:i:s')
    ];
    $pdo->prepare("INSERT INTO foo_log (params, time) VALUES (?, ?);")
        ->execute($args);
} catch (PDOException $ex) {
    // Handle error here.
}

Mayor seguridad, brevedad y mejor legibilidad. ¿Qué más se puede pedir?

3. Otro error: no entender del todo para qué sirve PDO

Un error muy frecuente es no entender del todo para qué existe PDO y terminar usándolo como un simple ayudante de actualización.

Mientras juegan con PDO, casi todos los usuarios de PHP terminan considerándolo como una especie de mejora para la consulta INSERT / UPDATE , que acepta una matriz asociativa y crea una consulta con marcadores dinámicos, que básicamente se parece a esto (un código real tomado de una pregunta sobre Stack Overflow ):

 $params = []; 
 $setStr = "" ; 
 foreach ( $data as $key => $value ) 
 { 
    if ( $key != "id" ) 
    { 
 $setStr .= $key . " = :" . $key . "," ; 
    } 
 $params [ ':' . $key ] = $value ; 

 } 
 $setStr = rtrim ( $setStr , "," ); 
 $pdo -> prepare ( "UPDATE users SET $setStr  WHERE id = :id" )-> execute ( $params );

Se ve bastante bien, ya que puede producir rápidamente una consulta dentro de una matriz o arreglo en la que las claves representan nombres de columna y los valores representan los valores que se utilizarán en la consulta. Dado que los nombres de campos en formato HTML son los mismos que los nombres de columna de tabla, puede automatizar en gran medida el procesamiento de formularios, permitiéndole utilizar el mismo código para editar información en cualquier tabla. Muy conveniente. Pero catastróficamente vulnerable.

"¿Cómo?" - usted diría probablemente - "los marcadores de posición se utilizan y los datos se enlazan con seguridad, por lo tanto nuestra consulta es segura". Sí, los datos son seguros. Pero, de hecho, lo que hace este código es tomar la entrada del usuario y agregarlo directamente a la consulta. Sí, es una variable $key. Que va a la derecha en su consulta sin tratamiento.

Usar PDO de forma ingenua, y mal configurado, es una puerta abierta a la inyección SQL.

4. Ejemplos de inyección posibles con PDO mal configurado y desmontando algunos mitos sobre protección de la base de datos

El punto 4 sólo aplica si tenemos un PDO mal configurado. Aunque es un poco extenso lo he querido traducir para que podamos apreciar el alcance de un error tan fácil de cometer. También desmonta algunos mitos como el saneamiento de nuestras cadenas, erróneamente recomendado en muchos sitios de internet.

Aquí hay una pequeña prueba de código de concepto, que, siempre y cuando tenga una tabla "usuarios" y una fila con id = 1, cambiará el nombre de usuario en la tabla a un resultado de la consulta SELECT. Y esta consulta podría ser cualquier cosa:

 <form method = POST> 
 <input type = hidden name = "name=(SELECT'hacked!')WHERE`id`=1#" value = ""> 
 <input type = hidden name = "name" value = "Joe"> 
 <input type = hidden name = "id" value = "1"> 
 <input type = submit> 
 </form> 
 <? php 
 if ( $_POST ) { 
 $pdo = new PDO ( 'mysql:dbname=test;host=localhost' , 'root' , '' ); 
 $params = []; 
 $setStr = "" ; 
    foreach ( $_POST as $key => $value ) 
    { 
        if ( $key != "id" ) 
        { 
 $setStr .= $key . " = :" . $key . "," ; 
        } 
 $params [ $key ] = $value ; 

    } 
 $setStr = rtrim ( $setStr , "," ); 
 $pdo -> prepare ( "UPDATE users SET $setStr  WHERE id = :id" )-> execute ( $params ); 
 }  

Este código producirá una consulta como esta

UPDATE users 
  SET name =
  ( SELECT 'hacked!' ) 
  WHERE ` id `= 1 # = :name=(SELECT'1')
  WHERE`id`=1#,name = :name WHERE id = :id  

Donde todo lo pasado después de # será tratado como un comentario.

Puedes probarlo en casa.

¿Qué sucede en este código? Estamos forjando un formulario, añadiendo otro campo a él, y escribiendo un código SQL en el atributo name. La función auxiliar mencionada tomará este SQL forjado y lo insertará en la consulta construida. Como resultado, en lugar de "Joe" el nombre se establecerá en "hackeado!". ¿No asusta demasiado, eh? Pero usted tiene que entender que lo que es peligroso aquí es el hecho de la inyección. Mientras que sea posible, la cantidad de crear consultas mal intencionadas es infinita. Y no todas son tan inofensivas. A continuación veremos una vulnerabilidad más peligrosa.

PDO :: quote () - el movimiento equivocado

Ok, necesitamos proteger nuestro código útil (así como cualquier otro código que no pueda ser protegido con declaraciones preparadas).

Lo primero que un usuario promedio de PHP probablemente pensaría es una función incorporada de PDO::quote() , que aparentemente hace lo que necesitamos - protege los datos de la inyección de SQL -. Pero pronto estará claro que una función destinada al formato de cadena es inaplicable para los identificadores. Ésta agregará comillas simples alrededor de valor devuelto y hará que nuestro código produzca una secuencia como esta:

  'name' = 'joe'  

Lo que resultará en un error de sintaxis, ya que las comillas simples son ilegales como delimitadores de nombre de campo en MySQL.

No pienses nunca en recortar comillas simples de la cadena resultante, ya que fue sugerido hace algún tiempo en un comentario fuertemente promocionado (ahora borrado) bajo la entrada de PDO::quote() en el manual de PHP: el propósito de esta función es hacer un formateo completo, haciendo escape de ambos caracteres y agregando comillas. Saca una de estas dos acciones y tendrás una inyección.

En otras palabras, si tiramos las comillas simples circundantes del resultado de PDO::quote() , será como si solo aplicáramos la picadura regular escapando. Y veamos por qué está mal:

Cadena de escape (mysql_real_escape_string) - un desastre

Otra cosa que un usuario de PHP podría pensar es una función de escape familiar, que "hace que sus datos sean seguros" como falsamente se declaró durante mucho tiempo en el manual de PHP. Desafortunadamente, solo ayudará si el código de inyección contiene comillas simples u otros caracteres con esta función. La mala noticia es que todos esos carácteres son innecesarios para la inyección, demostrando esta función inútil para el caso (y refutando las habilidades ficticias de protección de esta función):

 <form method = POST> 
 <input type = hidden name = "name=(SELECT password from admins)WHERE`id`=1#" value = ""> 
 <input type = hidden name = "name" value = "Joe"> 
 <input type = hidden name = "id" value = "1"> 
 <input type = submit> 
 </form> 
 <? php 
 if ( $_POST ) { 
 $pdo = new PDO ( 'mysql:dbname=test;host=localhost' , 'root' , '' ); 
 $params = []; 
 $setStr = "" ; 
    foreach ( $_POST as $key => $value ) 
    { 
        if ( $key != "id" ) 
        { 
 $setStr .= addslashes ( $key ). " = :" . $key . "," ; 
        } 
 $params [ $key ] = $value ; 

    } 
 $setStr = rtrim ( $setStr , "," ); 
 $pdo -> prepare ( "UPDATE users SET $setStr  WHERE id = :id" )-> execute ( $params ); 
 var_dump ( "UPDATE users SET $setStr  WHERE id = :id" , $_POST ); 
 }  
 ?>

Como se puede ver, no hay una sola cita en la consulta maliciosa y por lo tanto ni addslashes() ni varios *_escape_string() puede hacer nada aquí.

Esta inyección es mucho más peligrosa ya que revelará la contraseña del administrador a cualquier persona.

Adición de Operadores de ejecución o comillas invertidas `` (aún vulnerable)

Ok, como ya hemos aprendido, las comillas simples no ayudarán. ¿Quizá los operadores de ejecución o comillas invertidas (``) ayudarían? Veamos si ayudan:

  <form method = POST> 
 <input type = hidden name = "name`=(SELECT'hacked!')WHERE`id`=1#" value = ""> 
 <input type = hidden name = "name" value = "Joe"> 
 <input type = hidden name = "id" value = "1"> 
 <input type = submit> 
 </form> 

 <? php 
 if ( $_POST ) { 
 $pdo = new PDO ( 'mysql:dbname=test;host=localhost' , 'root' , '' ); 
 $params = []; 
 $setStr = "" ; 
    foreach ( $_POST as $key => $value ) 
    { 
        if ( $key != "id" ) 
        { 
 $setStr .= "` $key ` = : $key ," ; 
        } 
 $params [ $key ] = $value ; 

    } 
 $setStr = rtrim ( $setStr , "," ); 
 $pdo -> prepare ( "UPDATE users SET $setStr  WHERE id = :id" )-> execute ( $params ); 
 }
 ?>  

Como se puede ver, añadir comillas invertidas no nos ayudó en absoluto: un atacante sólo añade otra comilla para cerrar prematuramente el identificador y luego proceder con la consulta malintencionada. Sucede porque agregar cualquier delimitador alrededor de algún literal es inútil si no escapamos de estos delimitadores dentro - es la lección que aprendimos duramente de las inyecciones convencionales basadas en cadenas. ¡Así que escaparemos nuestras comillas invertidas!

Escapando las comillas invertidas

Entonces, ¿qué se puede hacer para conservar una función tan útil, pero sin una violación terrible?

Como se dijo antes, escapar delimitadores ayudaría. Por lo tanto, su primer nivel de defensa debe ser escapar delimitadores (comillas invertidas) duplicándolos.

Construyendo nuestra consulta de esta manera:

 $setStr .= "`" . str_replace ( "`" , "``" , $key ). "` = :" . $key . "," ;  

Usted conseguirá eliminar la inyección.

Si tratamos de inyectar, utilizando el método anterior, resultará en un error de ejecución de la consulta, que, aunque es seguramente mejor que una inyección exitosa, todavía no es un comportamiento bastante deseable.

Esta es la razón por la que citar / escapar nombres de campo aún no es suficiente. Y esta es la razón por la que usted debe utilizar otro nivel de defensa:

Solución sólida en todo

Hay una cosa más a tener en cuenta: aunque citar / escapar nombres de campo eliminará la inyección de SQL clásica, hay otro vector de ataque posible. Como no controlamos qué nombres de campo se utilizan para actualizar, es posible que el usuario modifique el valor al que no debería tener acceso. Por ejemplo, imagine que hay un campo en una tabla de usuarios como admin que se establece en 1 para admins y 0 para usuarios regulares. La función en cuestión permitirá a cualquier manipulador de código aumentar el nivel de privilegio de su cuenta.

Suponiendo todo lo anterior, una función tan útil debe estar siempre aceptando un parámetro adicional con una lista de nombres de campos permitidos:

  <form method = POST> 
    <input type = hidden name = "name`=(SELECT'hacked!')WHERE`id`=1#" value = ""> 
    <input type = hidden name = "name" value = "Joe"> 
    <input type = hidden name = "id" value = "1"> 
    <input type = submit> 
    </form> 

 <? php 
 if ( $_POST ) { 
 $pdo = new PDO ( 'mysql:dbname=test;host=localhost' , 'root' , '' ); 
 $allowed = [ "name" , "surname" , "email" ]; 

 $params = []; 
 $setStr = "" ; 
    foreach ( $allowed as $key ) 
    { 
        if (isset( $_POST [ $key ]) && $key != "id" ) 
        { 
 $setStr .= "`" . str_replace ( "`" , "``" , $key ). "` = :" . $key . "," ; 
 $params [ $key ] = $_POST [ $key ]; 
        } 
    } 
 $setStr = rtrim ( $setStr , "," ); 
 $params [ 'id' ] = $_POST [ 'id' ]; 
 var_dump ( "UPDATE users SET $setStr  WHERE id = :id" , $params , $_POST ); 
 $pdo -> prepare ( "UPDATE users SET $setStr  WHERE id = :id" )-> execute ( $params ); 
 }
 ?>  

¡Con tal mejora nuestro código se convertirá en sólido en todo!

Por supuesto, los ejemplos anteriores (punto 4) sólo funcionarán si el modo de emulación está activado. Pero usted tiene que entender que si la inyección del SQL puede ser explotada con éxito o no es una pregunta diferente y completamente irrelevante. Usted no debe dejar una inyección en el primer lugar. O la manera de explotarlo se encontrará, un día u otro.


5. Entonces, las consultas preparadas sirven o no?

Sí, pero a condición de que nuestro PDO esté configurado como se indica en el apartado 2

5.1 Cómo prevenir inyección de SQL (casi) siempre* garantizado

Utilice las instrucciones preparadas , también conocidas como consultas parametrizadas . Por ejemplo:

/**
 * Note: This code is provided for demonstration purposes.
 *       In general, you want to add some application logic to validate
 *       the incoming parameters. You do not need to escape anything.
 */
$stmt = $pdo->prepare('SELECT * FROM blog_posts WHERE YEAR(created) = ? AND MONTH(created) = ?');
if ($stmt->execute([$_GET['year'], $_GET['month']])) {
    $posts = $stmt->fetchAll(\PDO::FETCH_ASSOC);
}

Las declaraciones preparadas eliminan cualquier posibilidad de inyección de SQL en su aplicación web. No importa lo que se pasa a las variables $_GET aquí, la estructura de la consulta SQL no puede ser cambiada por un atacante (a menos que, por supuesto, tenga PDO::ATTR_EMULATE_PREPARES activado, lo que significaría que no está usando auténticas declaraciones).

Nota: Si usted intenta desactivar PDO::ATTR_EMULATE_PREPARES , es posible que algunas versiones de manejadores de bases de datos ignoren este intento. Para tomar precauciones extras, establezca explícitamente el conjunto de caracteres en el DSN a uno que su aplicación y base de datos utilizan (por ejemplo UTF-8 , el cual, si está usando MySQL, se llama confusamenteutf8mb4` ).

Los estados preparados solucionan un problema fundamental de la seguridad de la aplicación: Separan los datos que se van a procesar de las instrucciones que operan sobre dichos datos enviándolos en paquetes completamente separados. Este es el mismo problema fundamental que hace provoca los desbordamientos de pila.

Siempre y cuando nunca concatenes las variables proporcionadas por el usuario o el entorno con la sentencia SQL (y asegúrandote de no usar preparaciones emuladas) puedes, para todos los propósitos, descartar la inyección de SQL de tu lista de preocupaciones para siempre.

***Advertencia Importante y Clarificación **** Las sentencias preparadas protegen las interacciones entre su aplicación web y su servidor de base de datos (si están en máquinas separadas, también deben comunicarse a través de TLS). Todavía es posible que un atacante pueda almacenar una carga útil en un campo que podría ser peligroso, por ejemplo, en un procedimiento almacenado. Llamamos a esto una inyección SQL de orden superior (la respuesta de Stack Overflow vinculada se refiere a ellos como de "segundo orden", pero cualquier cosa que se ejecute después la consulta inicial debería ser objeto de análisis).

En esta situación, nuestro consejo sería no escribir procedimientos almacenados de tal manera que creen puntos de inyección SQL de orden superior.

5.2 ¿Qué pasa con el saneamiento de las entradas (Sanitizing Input)?

Si bien es posible prevenir ataques mediante la reescritura del flujo de datos entrantes antes de enviarlo al controlador de la base de datos, esta práctica está llena de matices peligrosos y oscuros... (Ambos enlaces en la oración anterior son muy recomendables.)

A menos que desee tomar el tiempo para investigar y lograr un dominio completo sobre todos los formatos Unicode que su aplicación use o acepte, es mejor que no intente desinfectar sus entradas. Las sentencias preparadas son más eficaces en la prevención de la inyección de SQL que las cadenas de escape.

Además, alterar el flujo de datos entrantes puede causar daños en los datos, especialmente si se trata de bloques binarios crudos (por ejemplo, imágenes o mensajes cifrados).

Las sentencias preparadas son más fáciles y pueden garantizar la prevención de la inyección de SQL.

Si la entrada del usuario nunca tiene la oportunidad de alterar la cadena de consulta, nunca puede conducir a la ejecución de código. Las sentencias preparadas separan completamente el código de los datos.

5.3 La entrada debe ser validada

La validación no es lo mismo que el saneamiento. Las sentencias preparadas pueden impedir la inyección de SQL, pero no pueden guardarlo de datos incorrectos. Para la mayoría de los casos, filter_var() es útil aquí.

$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
if (empty($email)) {
    throw new \InvalidArgumentException('Invalid email address');
}

Nota: filter_var() valida que la cadena de correo electrónico dada se ajusta a la especificación RFC. No garantiza que haya una bandeja de entrada abierta en esa dirección ni compruebe que el nombre de dominio esté registrado. Una dirección de correo electrónico válida todavía no es segura para usar en consultas sin procesar, ni para mostrar en una página web sin filtrar para evitar ataques XSS .

5.4 ¿Qué pasa con los identificadores de columnas y tablas?

Dado que los identificadores de columna y tabla forman parte de la estructura de consulta, no es posible parametrizarlos. Por lo tanto, si la aplicación que está desarrollando requiere una estructura de consulta dinámica donde las tablas o columnas son seleccionadas por el usuario, debe optar por una lista blanca.

Una lista blanca es una estrategia de lógica de aplicación que explícitamente sólo permite unos cuantos valores aceptados y rechaza el resto o utiliza un predeterminado sano. Contrasta con una lista negra, que sólo prohíbe las entradas mal conocidas. En la mayoría de los casos, las listas blancas son mejores para la seguridad que las listas negras.

$qs = 'SELECT * FROM photos WHERE album = ?';
// Use switch-case for an explicit whitelist
switch ($_POST['orderby']) {
    case 'name':
    case 'exifdate':
    case 'uploaded':
       // These strings are trusted and expected
       $qs .= ' ORDER BY ' . $_POST['orderby'];
       if (!empty($_POST['asc'])) {
           $qs .= ' ASC';
       } else {
           $qs .= ' DESC';
       }
       break;
    default:
       // Some other value was passed. Let's just order by photo ID in descending order.
       $qs .= ' ORDER BY photoid DESC';
    }
$stmt = $db->prepare($qs);
if ($stmt->execute([$_POST['album_id']])) {
    $photos = $stmt->fetchAll(\PDO::FETCH_ASSOC); 
}

Si permite que el usuario final proporcione los nombres de la tabla y / o de las columnas, ya que los identificadores no pueden parametrizarse, debe recurrir a escapar. En estas situaciones, recomendamos lo siguiente:

No : Simplemente escriba los meta caracteres SQL (por ejemplo ' ) Sí : Filtre todos los caracteres que no están permitidos. El siguiente fragmento de código sólo permitirá nombres de tablas que comiencen con una letra en mayúscula o minúscula, seguida de cualquier número de caracteres alfanuméricos y subrayados.

if (!preg_match('/^[A-Za-z][A-Za-z0-9_]*$/', $table)) {
    throw new AppSpecificSecurityException("Possible SQL injection attempt.");
}
// And now you can safely use it in a query:
$stmt = $pdo->prepare("SELECT * FROM {$table}");
if ($stmt->execute()) {
    $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
}

5.6 ¿Qué pasa si el uso de declaraciones preparadas parece demasiado engorroso?

La primera vez que un desarrollador encuentra sentencias preparadas, puede sentirse frustrado por la perspectiva de verse forzado a escribir mucho código redundante (preparar, ejecutar, buscar, preparar, ejecutar, buscar, ad nauseam ).

Existe una biblioteca de PHP llamada EasyDB, que puede servir como alternativa al uso de PDO.

6. Enlaces

  1. PDO y MySQLi nos protegen de la inyección SQL primaria solamente
  2. Inyección SQL de segundo nivel
  3. Muy buen artículo con algunas recomendaciones para el uso de PDO
A. Cedano
  • 86,578
  • 19
  • 122
  • 221
60

La respuesta aceptada es un gran aporte, siempre se agradece. Hace tiempo leí también un artículo en internet que para mi parecer es bastante interesante y quisiera compartir, lástima que está en Ingles, voy a traducir lo mejor que pueda una parte del artículo, ya que es muy extenso (The Hitchhiker's Guide to SQL Injection prevention).

Una respuesta rápida seria:

Cómo proteger ante la inyección del tipo [xxx].

Usted puede ser atacados de diferentes tipos de inyecciones - "ciega", "retraso de la regla", "segundo orden" y miles de otros. Uno tiene que entender que todas estas no son maneras diferentes de realizar una inyección, pero apenas diversas maneras de explotarla. Aunque sólo hay una forma de realizar una inyección: romper la integridad de la consulta. Por lo tanto, si puede mantener la integridad de la consulta, estará a salvo de todos los miles de tipos diferentes de inyecciones a la vez.

Y para mantener la integridad de la consulta, basta con dar formato a los literales de consulta correctamente.

Conclusión.

En resumen, podemos formular dos reglas simples:

Incluso creada dinámicamente, una consulta SQL debe consistir en 2 tipos posibles de datos solamente:

  1. Partes constantes codificadas en el guion
  2. Marcadores de posición para cada valor dinámico

Cuando se siguen, estas reglas garantizarán una protección del 100%.

Veamos una respuesta más detallada a la prevención de inyección SQL:

1) ¿Qué es la inyección de SQL?

El origen del problema de la inyección SQL es la mezcla del código y los datos. De hecho, nuestra consulta SQL es un programa. Un programa legítimo y completa, al igual que nuestros scripts PHP familiares. Y así sucede que estamos creando esta aplicación de forma dinámica, agregando algunos datos sobre su marcha. Por lo tanto, estos datos pueden interferir con nuestro código y alterarlo. Tal alteración sería la propia inyección.

Esto solo puede ocurrir si no formateamos las partes de nuestra consulta de manera invulnerable.

Veamos un ejemplo canónico:

$nombre = 'Foo'; 
$sentencia = "SELECT * FROM usuario WHERE nombre='$nombre'";

Que se compila en secuencia maliciosa.

SELECT * FROM usuario WHERE nombre='Foo';

¿Lo llaman inyección? Incorrecto. Es un formateado inapropiada de una cadena literal.

Mientras se formatea correctamente, no dañará a nadie:

SELECT * FROM usuario WHERE nombre='Foo\';

Vamos a tomar otro ejemplo canónico,

$id    = "1";
$id    = mysqli_real_escape_string($conexion, $id);
$query = "SELECT * FROM usuario where id = $id";

Con resultado menos dañino:

SELECT * FROM usuario WHERE id =1;

¿Llámelo inyección de nuevo? Una vez más mal. Se trata de un literal numérico con formato incorrecto. Ya que se trate de un formato adecuado, una honesta

SELECT * FROM usuario where id = 1;

La declaración sería positivamente inofensiva.

Nota: Todo EL peligro está viniendo de la declaración de la misma pregunta: los millones de usuarios de PHP todavía creen que el propósito muy conocido de la función de mysql_real_escape_string() es "para proteger el SQL contra inyecciones" (escapando algunos "caracteres peligrosos" de manera ficticia). ¡Si supieran el verdadero propósito de esta función honesta, no habría inyecciones en el mundo! Si sólo formatearan sus consultas, en lugar de "protegerlas", habrían tenido una protección real como resultado.


2) ¿Cuáles son las reglas de formato?

La verdad es que las reglas de formato no son tan fáciles y no se pueden expresar en un solo imperativo. Para MySQL sería:

1. Cadenas

  • Tienen que ser agregados vía la sentencia preparada nativa o tienen que estar entre comillas los caracteres especiales y tienen que ser escapados.
  • La codificación correcta del cliente debe o puede estar codificado en Hexadecimal.

2. Números

  • Han de añadirse a través de declaración preparada nativa o debe cumplir el formato para contener sólo números, un delimitador decimal y un signo.

3. Identificadores

  • Tienen que ser encerrado entre comillas sencillas.
  • Caracteres especiales (francamente - los acentos abiertos muy delimitadoras) tienen que ser escapados.

4. Operadores y palabras clave

  • No hay reglas especiales de formato para las palabras clave y los operadores, además de que tienen que ser operadores legítimos de SQL y palabras clave. Así pues, tienen que ser en la lista blanca.

Como se puede ver, hay cuatro diferentes conjuntos de reglas, no sólo una única instrucción.


3) Declaraciones preparadas

La idea de una sentencia preparada nativa es inteligente y simple: la consulta y los datos se envían al servidor separados entre sí, y por lo tanto no hay posibilidad de que interfieran. Lo que hace imposible la inyección. Pero al mismo tiempo, la implementación nativa tiene sus limitaciones, ya que sólo soporta dos tipos de literales (cadenas y números, a saber) que los hace insuficientes e inseguros para el uso de la vida real.

Y aquí llegamos al punto principal: la idea general de crear una consulta SQL fuera de la parte constante y los marcadores de posición, que se sustituirá con datos reales, que se formatearán automáticamente es de hecho un Santo Grial que estábamos buscando.

El principal y más esencial beneficio de las declaraciones preparadas es la eliminación de todos los peligros del formato manual:

  • Declaración preparada hace el formateo completo. ¡Todo ello sin la intervención del programador!
  • Declaración preparada que hace el formato adecuado (siempre y cuando estamos de unión a nuestros datos utilizando el tipo adecuado).
  • Las declaraciones preparadas hacen el formateo invulnerable.
  • Declaración preparada tiene el formato en el único lugar adecuado - justo antes de la ejecución de la consulta.

Es por esto que el formato manual está tan despreciado en la actualidad y declaraciones preparadas son tan honrado.

mysqli_prepare — Prepara una sentencia SQL para su ejecución:

$mi_conexion = new mysqli("servidor", "usuario", "contraseña", "basedatos");

$id = $_POST['id'] ?: '';

//la consulta y los datos se envían al servidor separados entre sí, lo que evita posible inyección.
$stmt = $mi_conexion->prepare("SELECT nombre FROM usuario WHERE id=?"); //Consulta
$stmt->bind_param("i",$id);//Datos
$stmt->execute();//Ejecutamos sentencia.


4) Algunas medidas falsas y malas prácticas

1. Escapando la entrada del usuario.

Este es un rey. Una ilusión grave, aún compartida por casi todos los usuarios de PHP (e incluso OWASP, como se puede ver). Consta de dos partes: "Escapando" y "Entrada del usuario":

  1. Escapando: como hemos señalado anteriormente, hace sólo parte del trabajo, para sólo un tipo literal. Y cuando se usa solo o no en el lugar adecuado es un llamado seguro para el desastre.

  2. Entrada del usuario: no debe haber tales palabras en el contexto de la protección de inyección. ¡Cada variable es potencialmente peligrosa - no importa la fuente! O, en otras palabras, todas las variables tienen que estar correctamente formateadas para ser puestas en la consulta - no importa la fuente de nuevo. Es el destino lo que importa. En el momento en que un desarrollador empieza a separar las ovejas de las cabras, hace su primer paso hacia el desastre.

2. Validación de datos.

Uno tiene que entender, que la entrada (en el significado de la entrada del usuario) validación de datos no tiene absolutamente nada que ver con SQL. De Verdad. Ninguna regla de validación puede ayudar contra la inyección de SQL si se permite un texto de forma libre. Sin embargo, tenemos que dar formato a nuestro SQL a pesar de cualquier validación de todos modos - recuerda Sarah O'Hara que lleva un nombre que es perfectamente válido desde el punto de vista de entrada del usuario. También recuerde que las reglas de validación pueden cambiar.

3. Htmlspecialchars (y también filter_var(), strip_tags() y similares).

Amig@s. Es la codificación HTML, si aún no lo notaste. No tiene absolutamente nada que ver con SQL. No ayuda nada en la materia, y nunca se debe utilizar en el contexto de la protección de la inyección del SQL. Es absolutamente inaplicable para SQL, y no puede proteger su consulta incluso si se utiliza como una función de escape de cadena. Dejarlo para otras partes de su aplicación. Además, por favor entienda, que el formato SQL nunca debería tocar los datos. Entonces usted pone sus joyas en una caja fuerte, usted la quiere guardar intacto, ¡no algunas piezas modificadas o sustituidas! Igual que aquí. Una base de datos está destinada a almacenar sus datos, no a "proteger". Y es esencial para almacenar los datos exactos que desea volver a utilizarlos (significa que su intento tonto base64 también está mal, por cierto).

Diablo
  • 6,417
  • 2
  • 21
  • 40
28

Otra recomendación aparte de la buena respuesta principal

Es recomendable adicionar un enmascaramiento a la url y crear urls amigables que realizan 2 funciones: 1- ocultan parámetros que se estén enviando por get, y también ayudan al usuario a ver más clara la información.

Ejemplo

url standard :http://stackoverflow.com?id=100
url amigable :¿Cómo evitar la inyección SQL en PHP?

Nota:La primera url no corresponde a la url amigable y es sólo un ejemplo.

En el caso que el atacante deseara atacar la url standard, ya tendría un parámetro para empezar "id", mientras que en el caso de la url amigable no queda tan sencillo y debe analizar más a fondo la petición hecha al servidor web y encontrar la url.

Medidas Básicas

Cómo enmascaro la url

Un ejemplo es modificando el archivo .htaccess en tu servidor web, y unirlo con mod_rewrite si usas Apache.

Más información sobre esto aca: http://www.emenia.es/como-crear-urls-amigables-con-htaccess/

Si piensas que no es suficiente

Sería recomendable combinar el uso de login, rastreo de cookies, headers, agente web.

Además sumarle un sistema de captcha para evitar multiples peticiones generadas por robots.

Jhovanny Uribe
  • 438
  • 4
  • 9
  • 7
    Eso ni siquiera alcanza ser "segurar por ofuscar". un pésimo consejo. Que impacto tiene reescribir la url a parámetros POST por ejemplo? – Stefan Nolde Feb 03 '17 at 04:33
  • 6
    **Di unas medidas** --- **Básicas**, si con sólo una forma de escribir el código se pudieran contener todos los puntos de inyección no se tendría tantos puntos de inyección. Stefan lo invito a que con su sabiduría nos explique cómo disminuir las inyecciones de SQL en peticiones de tipo POST – Jhovanny Uribe Feb 03 '17 at 13:44
  • 3
    Creo que lo que Stefan intenta decir es que ofuscar los parámetros hace más difícil para el usuario común ver el nombre de los parámetros que se pasan, pero realmente no se previene nada porque el atacante (que puede no ser una persona) puede verlos de todos modos. – Alvaro Montoro Feb 03 '17 at 15:07
  • 7
    @JhovannyUribe la respuesta ya esta. 1. no confiar en ningún dato del usuario, validar y escapar todos los valores que se pasan al query antes que se pasan al query. Esta explicado en la otra respuesta. El punto es que "friendly urls" no tienen pero nada que ver con seguridad, pero solamente con la comodidad del usuario y facilitar la busqueda en las maquinas de busqueda. La razon porque encuentro una respuesta así peligroso es que alguién que **no** entiende eso podría pensar que hubiera mejorado su seguridad usando "friendly urls", para luego despertar en un mundo de dolor. – Stefan Nolde Feb 03 '17 at 16:23
  • 7
    Esto **no evita la inyección SQL** de ninguna manera, en absoluto. El ejemplo mostrado presenta el id dentro de la url `http://es.stackoverflow.com/questions/18232` y no evita nada, sólo la hace *más lindo* ver – Mariano Feb 12 '17 at 07:15
19

Mi respuesta simple, vamos al grano, lo que debes hacer ademas de un sistema de login y sesión de usuarios, es "validaciones" (especialmente en el login), crear un pattern y compararlo con lo que viene del post, (ademas de las validaciones básicas disponibles con html ) si es válido que lo deje pasar, algo así:

public function index(){
            if ($_POST) { 
               $permitidos = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@-_.'; 
                for ($i=0; $i<strlen($_POST['username']); $i++){ 
                 if (strpos($permitidos, substr($_POST['username'],$i,1))===false){ 

                  $da = true;
                  return $da; 
                  }else{    
                 }
             }...   

Este filtro determina si la cadena de texto contiene caracteres que no están en tu lista de permitidos, después si pasa por el filtro, el resto del código que seria lo que haría, guardar actualizar etc.

luego capturas el $da(datos del return) y lo muestras en el front como variable, para que si existe algo en el return, despliegue un mensaje de error en el formulario.

Por otro lado también deshabilita que puedan copiar y pegar en los inputs del formulario.

<input type="text" class="form-control" id="username" required name="username" onpaste="return false">

En resumen: debes evitar que ingresen caracteres especiales que se usan en las consultas sql, como * ; etc.

BastianBurst
  • 369
  • 1
  • 4
  • 17