Evitar sql injection en PHP con la clase PDO

Mejorar la seguridad de nuestras consultas con los metodos de la clase PDO, y evitar SQL Injection.

bases de datos conexión php PDO consultas sql

Si desarrollamos aplicaciones web complejas, o al menos con un uso constante de consultas sobre base de datos, deberemos preocuparnos por la seguridad de nuestra información, y por consiguiente asegurar un funcionamiento correcto de nuestra web. Debemos evitar ponérselo fácil al uso malintencionado de nuestro sitio, con el objetivo de perjudicarlo, destruirlo u obtener información que tenemos almacenada.

¿Qué es SQL Injection?


SQL Injection es una vulnerabilidad de nuestros sitios que permite a un atacante cualquiera realizar consultas a una base de datos, para ello se vale de un incorrecto filtrado de la información que se pasa a través de los campos y/o variables de un sitio web. Estos son por lo general usados para extraer credenciales y realizar accesos ilegítimos. Un fallo de este tipo puede llegar a permitir ejecución de comandos en el servidor, subida y lectura de archivos, o peor aún, la alteración total de los datos almacenados.

Una vulnerabilidad de tipo SQL Injection puede ser explotada tanto a través del método GET como del método POST de un formulario HTML. La práctica más común es hacerlo a través del GET, sin embargo hacerlo a través del método POST puede llegar a devolver los mismos resultados.

La información ampliada la podéis encontrar en este enlace: SQL Injection.

¿Qué puedo hacer para evitar el SQL Injection?

Para mejorar la seguridad de nuestras aplicaciones web, y evitar las distintas formas de la vulnerabilidad, podemos servirnos de diferentes estrategias, aunque en php, de forma nativa se incluyen cada distintas soluciones para este fin. Se trata entonces de conocer el problema, ser metódico en nuestros desarrollos, e intentar utilizar las mejores herramientas para estos fines.

Entre otra de mis tantas alabanzas a la clase PDO, voy a añadir una más, la que trataré a continuación y que nos permite de una forma simple mejorar la seguridad de nuestras consultas de forma drástica.

Métodos de la clase PDO que filtran los valores

En concreto, para la clase PDOStatement disponemos del metodo bindParam. Este metodo nos permite validar valores que proceden directa o indirectamente de variables de formulario y que más tarde utilizamos en el texto de nuestras consultas. Hay 2 usos posibles para ese método bindparam, será a nuestra elección elegir cual nos gusta más.

1. PDOStatement::bindParam por posición de variable

La primera forma de uso que voy a explicar es por posición de la variable. Esto lo lograremos insertando el símbolo interrogante "?" donde concatenaríamos nuestras variables php. Según el número de interrogantes será la posición del valor a introducir. El primer interrogante será la posición 1, y así consecutivamente con el resto.

En el siguiente ejemplo voy a mostrar como realizar todo el proceso desde la creación del objeto PDO hasta una consulta de actualización para una tabla usuario con las columnas nombre y id.

<?php
$new_nombre = $_GET['nom'];
$id_user = $_GET['user'];
$objetoPDO = new PDO('mysql:host=localhost;dbname=prueba','root', '');
$query = "UPDATE usuario SET nombre = ? WHERE id = ?";
$objetoPDOStat = $objetoPDO->prepare($query);
$objetoPDOStat->bindParam(1, $new_nombre, PDO::PARAM_STR);
$objetoPDOStat->bindParam(2, $id_user, PDO::PARAM_INT);
$exito = $objetoPDOStat->execute();
if($exito){
     $filas_resultado = $objetoPDOStat->fetchAll();
}
?>

En el código del ejemplo se puede ver como recojo por claridad dos variables que vienen de un formulario GET y las asigno a dos variables php, una de tipo texto $new_nombre y la otra un número entero que es el identificador de la tabla $id_user. A continuación, después de crear el objeto de la conexión, creo mi consulta colocando símbolos "?" donde normalmente colocaría las variables directamente. Cuando uso el método bindParam del objeto PDOStatement puedes ver como indico la posición del interrogante (1 en el caso del nombre y 2 para el id), y en el tercer parámetro del método introduzco la constante PDO del tipo de valor que le corresponde. De esta forma bindParam validará que estoy introduciendo un valor del tipo indicado y podrá dar error en caso de uso erróneo o fraudulento.

Los tipos de valor a introducir en el bindParam que le corresponden a los tipos de la base de datos los puedes ver en el siguiente enlace: data_type bindParam.

2.PDOStatement::bindParam por nombre de variable

La segunda forma de utilizar bindParam es tan válida como la primera y tan solo depende de tu gusto elegir entre una y otra. A diferencia de la anterior esta implementación es más gráfica, probablemente más intuitiva, aunque creo que te puedes equivocar de forma fácil en una letra y volverte loco busca el problema. Con el siguiente ejemplo lo entenderás:

<?php
$new_nombre = $_GET['nom'];
$id_user = $_GET['user'];
$objetoPDO = new PDO('mysql:host=localhost;dbname=prueba','root', '');
$query = "UPDATE usuario SET nombre = :nombre WHERE id = :id";
$objetoPDOStat = $objetoPDO->prepare($query);
$objetoPDOStat->bindParam(':nombre', $new_nombre, PDO::PARAM_STR);
$objetoPDOStat->bindParam(':id', $id_user, PDO::PARAM_INT);
$exito = $objetoPDOStat->execute();
if($exito){
     $filas_resultado = $objetoPDOStat->fetchAll();
}
?>

Como podrás observar la diferencia radica en esos nombres de variables ficticias con los ":" delante. El funcionamiento es idéntico al método anterior, y las constantes de tipo exactamente las mismas. Sí que te recomiendo que te ayudes de un buen editor de código fuente como Sublime Text para evitar errores al escribir variables.

Extra: bindParam o bindValue

Aunque en esta publicación no he tratado la otra alternativa que ofrece el método bindValue de PDOStatement, si que me gustaría nombrarla y comentar diferencias principales y usos.

El método bindValue se usa exactamente igual que bindParam, la diferencia radica en que bindParam recoge las variables por referencia, mientras que bindValue las recoge por valor. En un uso como en el de mis ejemplos no variaría en nada su funcionalidad, pero cuando desarrollamos código más complejo o encapsulamos el funcionamiento de este objeto PDOStatement en una clase propia nuestra o mediante funciones pueden ocurrirnos errores derivados de su mal uso. 

<?php
function execute_query_test(){
      $new_nombre = $_GET['nom'];
      $id_user = $_GET['user'];
      $objetoPDO = new PDO('mysql:host=localhost;dbname=prueba','root', '');
      $query = "UPDATE usuario SET nombre = :nombre WHERE id = :id";
      $objetoPDOStat = $objetoPDO->prepare($query);
      $objetoPDOStat->bindParam(':nombre', $new_nombre, PDO::PARAM_STR);
      $objetoPDOStat->bindParam(':id', $id_user, PDO::PARAM_INT);
 
      return $objetoPDOStat;
      //ERROR!!! las variables $new_nombre y $id_user son locales y por lo tanto son destruidas al salir de la función.
}
?>

Como he dicho bindParam es susceptible de producir errores si lo encapsulamos en funciones o clases, ya que al recibir las variables por referencia, si estas son de ámbito local  si el execute no se realiza en el mismo ámbito se perderían la referencia a esas variables pasadas como valor. En el ejemplo anterior se puede ver que devuelvo el objeto $objetoPDOStat con la consulta preparada con las variables $id_user o $new_nombre antes de realizar el execute y recuperar el resultado, por lo tanto en este caso o cualquier otro similar, estas se perderían en el objeto $objetoPDOStat y por lo tanto la consulta daría error en el lugar de código que realicemos el execute(). Si te ocurre no incurras en la ira y la locura y recuerda este consejo :D.

¿Y entonces por que no usar bindValue en vez de bindParam?

Pues bueno, bindParam te permite lanzar y relanzar una misma consulta habiendola validado tan solo una vez. ¿Qué como es posible esto? Pues gracias a su uso de variables por referencia podemos cambiar el valor de las variables "bindeadas" y volver a realizar un execute de la consulta y una posterior obtención de valores. Mejor lo vemos con este último ejemplo:

<?php
$new_nombre = $_GET['nom'];
$id_user = $_GET['user'];
$objetoPDO = new PDO('mysql:host=localhost;dbname=prueba','root', '');
$query = "UPDATE usuario SET nombre = :nombre WHERE id = :id";
$objetoPDOStat = $objetoPDO->prepare($query);
$objetoPDOStat->bindParam(':nombre', $new_nombre, PDO::PARAM_STR);
$objetoPDOStat->bindParam(':id', $id_user, PDO::PARAM_INT);
$exito = $objetoPDOStat->execute();
if($exito){
     $filas_resultado = $objetoPDOStat->fetchAll();
     //cambio los valores de las variables referenciadas y vuelvo a ejecutar
     $id_user = 24;
     $new_nombre = 'test';
     $exito = $objetoPDOStat->execute(); //segunda consulta con valores nuevos
}
?>

Sobre el autor

Javier Gómez Redactor en Srcodigofuente.es

Javier Gómez

Ingeniero técnico en informática de gestión. Desarrollador web freelance y profesor de desarrollo web a partes iguales. Testarudo autodidacta, creativo, perfeccionista y alma libre.

Cargando comentarios

Utilizamos "cookies" para información estadística. Si continúas navegando aceptas su uso.