Reemplazar funciones mysql_ * con PDO y declaraciones preparadas

Siempre he hecho la conexión simple de mysql_connect , mysql_pconnect :

 $db = mysql_pconnect('*host*', '*user*', '*pass*'); if (!$db) { echo("Error: Could not connect to the database!"); exit; } mysql_select_db('*database*'); 

Mientras uso esto, siempre he usado el método simple para escapar de cualquier información antes de hacer una consulta, ya sea INSERT , SELECT , UPDATE o DELETE usando mysql_real_escape_string

 $name = $_POST['name']; $name = mysql_real_escape_string($name); $sql = mysql_query("SELECT * FROM `users` WHERE (`name` = '$name')") or die(mysql_error()); 

¡Ahora entiendo que esto es seguro, hasta cierto punto!

Se escapa de personajes peligrosos; sin embargo, sigue siendo vulnerable a otros ataques que pueden contener caracteres seguros, pero que pueden ser perjudiciales para mostrar datos o, en algunos casos, modificar o eliminar datos maliciosamente.

Por lo tanto, busqué un poco y descubrí acerca de PDO, MySQLi y declaraciones preparadas. Sí, puedo llegar tarde al juego, pero he leído muchos, muchos tutoriales (tizag, W3C, blogs, búsquedas de Google) y ninguno ha mencionado estos. Parece muy extraño por qué, ya que escaparse de la entrada del usuario realmente no es seguro y no es una buena práctica para decir lo menos. Sí, soy consciente de que podrías usar Regex para abordarlo, pero aún así, estoy bastante seguro de que no es suficiente.

A mi entender, el uso de sentencias PDO / preparadas es una forma mucho más segura de almacenar y recuperar datos de una base de datos cuando las variables se dan por la entrada del usuario. El único problema es que el cambio (especialmente después de estar muy atascado en mis modos / hábitos de encoding previa) es un poco difícil.

Ahora entiendo que para conectarme a mi base de datos usando PDO, usaría

 $hostname = '*host*'; $username = '*user*'; $password = '*pass*'; $database = '*database*' $dbh = new PDO("mysql:host=$hostname;dbname=$database", $username, $password); if ($dbh) { echo 'Connected to database'; } else { echo 'Could not connect to database'; } 

Ahora, los nombres de las funciones son diferentes, por lo que mi mysql_query , mysql_fetch_array , mysql_num_rows , etc. ya no funcionarán. Así que tengo que leer / recordar una gran cantidad de nuevos, pero aquí es donde me confundo.

Si quisiera insertar datos de, por ejemplo, un formulario de registro / inscripción, ¿cómo podría hacerlo, pero principalmente cómo lo haré de forma segura? Supongo que aquí es donde entran las declaraciones preparadas, pero al usarlas ¿elimina esto la necesidad de usar algo como mysql_real_escape_string ? Sé que mysql_real_escape_string requiere que esté conectado a una base de datos a través de mysql_connect / mysql_pconnect así que ahora no estamos usando tampoco ¿esta función no solo generará un error?

También he visto diferentes maneras de abordar el método PDO, por ejemplo, he visto :variable y ? como lo que creo que se conocen como titulares de lugar (lo siento si eso está mal).

Pero creo que esto es más o menos la idea de qué se debe hacer para buscar un usuario de una base de datos

 $user_id = $_GET['id']; // For example from a URL query string $stmt = $dbh->prepare("SELECT * FROM `users` WHERE `id` = :user_id"); $stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT); 

Pero luego estoy atascado en un par de cosas, si la variable no era un número y era una cadena de texto, debes darle una longitud después de PDO:PARAM_STR si no me equivoco. Pero, ¿cómo se puede establecer una longitud establecida si no está seguro del valor dado por los datos introducidos por el usuario, puede variar cada vez? De cualquier manera, por lo que sé para mostrar los datos, entonces haces

 $stmt->execute(); $result = $stmt->fetchAll(); // Either foreach($result as $row) { echo $row['user_id'].'
'; echo $row['user_name'].'
'; echo $row['user_email']; } // Or foreach($result as $row) { $user_id = $row['user_id']; $user_name = $row['user_name']; $user_email = $row['user_email']; } echo("".$user_id."
".$user_name."
".$user_email."");

Ahora, ¿esto es seguro?

Si estoy en lo cierto, la inserción de datos sería la misma, por ejemplo:

  $username = $_POST['username']; $email = $_POST['email']; $stmt = $dbh->prepare("INSERT INTO `users` (username, email) VALUES (:username, :email)"); $stmt->bindParam(':username, $username, PDO::PARAM_STR, ?_LENGTH_?); $stmt->bindParam(':email, $email, PDO::PARAM_STR, ?_LENGTH_?); $stmt->execute(); 

¿Eso funcionaría, y también es seguro? Si es correcto, ¿qué valor pondría para ?_LENGTH_? ? ¿Tengo todo esto completamente mal?

ACTUALIZAR

Las respuestas que he tenido hasta ahora han sido extremadamente útiles, ¡no puedo agradecerles lo suficiente! Todos obtuvieron un +1 por abrir mis ojos a algo un poco diferente. Es difícil elegir la respuesta principal, pero creo que el Coronel Shrapnel se lo merece ya que todo está prácticamente cubierto, incluso yendo a otras matrices con bibliotecas personalizadas de las que no tenía conocimiento.

Pero gracias a todos ustedes 🙂

Gracias por la interesante pregunta. Aqui tienes:

Se escapa de personajes peligrosos,

Tu concepto es completamente incorrecto.
De hecho, “personajes peligrosos” es un mito, no hay ninguno. Y mysql_real_escape_string escapando pero solo un delimitador de cadena . A partir de esta definición, puede concluir que se trata de limitaciones: funciona solo para cadenas .

sin embargo, sigue siendo vulnerable a otros ataques que pueden contener caracteres seguros, pero que pueden ser perjudiciales para mostrar datos o, en algunos casos, modificar o eliminar datos maliciosamente.

Estás mezclando todo aquí.
Hablando de base de datos,

  • para las cuerdas NO es vulnerable. Siempre que sus cadenas sean entrecomilladas y escapadas, no pueden “modificar o eliminar datos maliciosamente”. *
  • para los otros tipos de datos de datos, sí, es inútil . Pero no porque sea algo “inseguro” sino solo por un uso inapropiado.

En cuanto a la visualización de datos, supongo que no es tópica en la pregunta relacionada con PDO, ya que PDO tampoco tiene nada que ver con la visualización de datos.

escapando de la entrada del usuario

^^^ ¡Otra ilusión que se nota!

  • la entrada de un usuario no tiene absolutamente nada que ver con el escape . Como puede aprender de la definición anterior, debe escapar cadenas, no lo que sea “entrada de usuario”. Entonces, de nuevo:

    • tienes cuerdas de escape, sin importar su origen
    • es inútil escapar de otros tipos de datos, sin importar la fuente.

¿Tienes el punto?
Ahora, espero que entiendas las limitaciones del escape así como el concepto erróneo de los “personajes peligrosos”.

A mi entender, el uso de declaraciones preparadas con PDO es mucho más seguro

Realmente no.
De hecho, hay cuatro partes de consulta diferentes que podemos agregar de forma dinámica:

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

entonces, puedes ver que escapar cubre solo un problema. (pero, por supuesto, si tratas los números como cadenas (poniéndolos entre comillas), cuando corresponda , también puedes protegerlos)

mientras que las declaraciones preparadas cubren – ugh – ¡2 ideas completas! Un gran problema 😉

Para los otros 2 problemas, vea mi respuesta anterior. En PHP, al enviar cadenas a la base de datos, ¿debo ocuparme de los caracteres ilegales usando htmlspecialchars () o usar una expresión regular?

Ahora, los nombres de las funciones son diferentes, por lo que mi mysql_query, mysql_fetch_array, mysql_num_rows, etc. ya no funcionarán.

Esa es otra gran ilusión de usuarios de PHP, un desastre natural, una catástrofe:

Incluso cuando se utiliza el antiguo controlador mysql, ¡ uno nunca debería usar funciones API desnudas en su código! ¡Hay que ponerlos en alguna función de biblioteca para el uso diario! (No como un rito mágico sino para hacer que el código sea más corto, menos repetitivo, a prueba de errores, más consistente y legible).

¡Lo mismo vale para la PDO también!

Ahora vuelve a tu pregunta.

pero al usarlos ¿elimina esto la necesidad de usar algo como mysql_real_escape_string?

SÍ.

Pero creo que esto es más o menos la idea de qué se debe hacer para buscar un usuario de una base de datos

¡No para buscar, sino para agregar cualquier dato a la consulta !

tienes que dar una longitud después de PDO: PARAM_STR si no me equivoco

Puedes, pero no tienes que hacerlo.

Ahora, ¿esto es seguro?

En términos de seguridad de la base de datos, simplemente no hay puntos débiles en este código. Nada para proteger aquí.

para la seguridad de visualización, solo busque en este sitio la palabra clave XSS .

Espero arrojar algo de luz sobre el asunto.

Por cierto, para las inserciones largas puede hacer un uso de la función que escribí algún día, Insertar / actualizar la función auxiliar con PDO

Sin embargo, no estoy usando declaraciones preparadas en este momento, ya que prefiero mis marcadores de posición elaborados en casa sobre ellos, utilizando una biblioteca que mencioné anteriormente. Entonces, para contrarrestar el código publicado por el riha a continuación, sería tan corto como estas 2 líneas:

 $sql = 'SELECT * FROM `users` WHERE `name`=?s AND `type`=?s AND `active`=?i'; $data = $db->getRow($sql,$_GET['name'],'admin',1); 

Pero, por supuesto, también puede tener el mismo código usando declaraciones preparadas.


* (yes I am aware of the Schiflett's scaring tales)

Nunca me molesto con bindParam () o param tipos o longitudes.

Acabo de pasar una matriz de valores de parámetros para ejecutar (), como este:

 $stmt = $dbh->prepare("SELECT * FROM `users` WHERE `id` = :user_id"); $stmt->execute( array(':user_id' => $user_id) ); $stmt = $dbh->prepare("INSERT INTO `users` (username, email) VALUES (:username, :email)"); $stmt->execute( array(':username'=>$username, ':email'=>$email) ); 

Esto es igual de efectivo y más fácil de codificar.

Puede que también le interese mi presentación Mitos y falacias de inyección de SQL , o mi libro SQL Antipatterns: Cómo evitar las trampas de la progtwigción de bases de datos .

Sí,: ¿algo es un marcador de posición nombrado en PDO? es un marcador de posición anónimo. Le permiten vincular valores uno por uno o todos a la vez.

Entonces, básicamente eso hace cuatro opciones para proporcionarle a su consulta valores.

Uno por uno con bindValue ()

Esto vincula un valor concreto a su marcador de posición tan pronto como lo llame. Incluso puede enlazar cadenas codificadas como bindValue(':something', 'foo') si lo desea.

Proporcionar un tipo de parámetro es opcional (pero sugerido). Sin embargo, dado que el valor predeterminado es PDO::PARAM_STR , solo necesita especificarlo cuando no es una cadena. Además, PDO se ocupará de la longitud aquí – no hay parámetro de longitud.

 $sql = ' SELECT * FROM `users` WHERE `name` LIKE :name AND `type` = :type AND `active` = :active '; $stm = $db->prepare($sql); $stm->bindValue(':name', $_GET['name']); // PDO::PARAM_STR is the default and can be omitted. $stm->bindValue(':type', 'admin'); // This is not possible with bindParam(). $stm->bindValue(':active', 1, PDO::PARAM_INT); $stm->execute(); ... 

Por lo general, prefiero este enfoque. Lo encuentro más limpio y más flexible.

Uno por uno con bindParam ()

Una variable está vinculada a su marcador de posición que se leerá cuando se ejecute la consulta, NO cuando se llame a bindParam (). Eso puede o no ser lo que quieres. Resulta útil cuando quiera ejecutar repetidamente su consulta con diferentes valores.

 $sql = 'SELECT * FROM `users` WHERE `id` = :id'; $stm = $db->prepare($sql); $id = 0; $stm->bindParam(':id', $id, PDO::PARAM_INT); $userids = array(2, 7, 8, 9, 10); foreach ($userids as $userid) { $id = $userid; $stm->execute(); ... } 

Solo prepara y vincula una vez que asegura los ciclos de la CPU. 🙂

Todo a la vez con marcadores de posición nombrados

Simplemente inserta una matriz para execute() . Cada clave es un marcador de posición en su consulta (ver la respuesta de Bill Karwins). El orden de la matriz no es importante.

En una nota al margen: con este enfoque no puede proporcionar PDO con sugerencias de tipos de datos (PDO :: PARAM_INT etc.). AFAIK, PDO intenta adivinar.

Todo a la vez con marcadores de posición anónimos

También inserta una matriz para ejecutar (), pero está indexada numéricamente (no tiene claves de cadena). Los valores reemplazarán sus marcadores de posición anónimos uno por uno en el orden en que aparecen en su consulta / matriz: el primer valor de matriz reemplaza al primer marcador de posición, y así sucesivamente. Ver la respuesta de erm410.

Al igual que con la matriz y los marcadores de posición nombrados, no puede proporcionar sugerencias de tipos de datos.

Lo que tienen en común

  • Todos ellos requieren que vincules / proporciones tantos valores como marcadores de posición. Si une demasiados / pocos, PDO se los comerá a sus hijos.
  • No tiene que preocuparse por escapar, PDO se encarga de eso. Las declaraciones PDO preparadas son seguras por inyección de SQL. Sin embargo, eso no es cierto para exec () y query () ; en general, solo debe usar esos dos para consultas codificadas.

También tenga en cuenta que PDO arroja excepciones . Esos podrían revelar información potencialmente sensible para el usuario. ¡Debería al menos poner su configuración inicial de PDO en un bloque de prueba / captura !

Si no desea arrojar Excepciones más adelante, puede configurar el modo de error como advertencia.

 try { $db = new PDO(...); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING) } catch (PDOException $e) { echo 'Oops, something went wrong with the database connection.'; } 

Para responder la pregunta sobre la longitud, especifique que es opcional, a menos que el parámetro que está vinculando sea un parámetro OUT de un procedimiento almacenado, por lo que en la mayoría de los casos puede omitirlo con seguridad.

En lo que respecta a la seguridad, el escape se realiza detrás de las escenas cuando se unen los parámetros. Esto es posible porque tuvo que crear una conexión de base de datos cuando creó el objeto. También está protegido de los ataques de inyección SQL ya que al preparar la statement, le está diciendo a su base de datos el formato de la statement antes de que la entrada del usuario pueda acercarse a ella. Un ejemplo:

 $id = '1; MALICIOUS second STATEMENT'; mysql_query("SELECT * FROM `users` WHERE `id` = $id"); /* selects user with id 1 and the executes the malicious second statement */ $stmt = $pdo->prepare("SELECT * FROM `users` WHERE `id` = ?") /* Tells DB to expect a single statement with a single parameter */ $stmt->execute(array($id)); /* selects user with id '1; MALICIOUS second STATEMENT' ie returns empty set. */ 

Por lo tanto, en términos de seguridad, sus ejemplos anteriores parecen estar bien.

Finalmente, estoy de acuerdo en que los parámetros de enlace individualmente son tediosos y se realizan con la misma eficacia con un conjunto pasado a PDOStatement-> execute () (vea http://www.php.net/manual/en/pdostatement.execute.php ).