Cómo evitar SQL Injection + Prepared Statements en PHP
Luego de escribir la serie de artículos sobre hacking usando SQL Injection, creo que es tiempo de escribir sobre algún método de prevención >=)

Como se puede observar en cualquier bibliografía "decente" sobre SQL Injection, existen básicamente 3 formas de prevenir la inyección, las cuales están muy bien descriptas en el artículo de OWASP SQL Injection Prevention Cheat Sheet. Las opciones son:

- Prepared Statements: Los prepared statements son sentencias pre-compiladas, en las cuales se indica qué parámetros serán ingresados por el usuario. De esta forma podemos indicarle al DBMS cuál es el código a ejecutar y cuáles serán las variables. Esto permite que el motor distinga la sentencia a ejecutar de los datos de entrada y así evitar que el usuario agregue sentencias SQL.
Además de la obvia ventaja de prevenir las inyecciones, éstos permiten mejorar el tiempo de ejecución si la misma sentencia se utiliza más de una vez. Cuando armamos una sentencia SQL, el motor de base de datos analiza, compila y optimiza la forma en que la ejecutará, por ello, si la armamos una sola vez y la ejecutamos varias veces (ya sea utilizando los mismos o diferentes parámetros), el tiempo de ejecución disminuirá considerablemente.

- Stored Procedures: Los stored procedures son más conocidos que los prepared statements entre los desarrolladores debido a su uso extensivo en la programación que interactua con BDs. Se escriben procedimientos en el lenguaje del DBMS (los cuales se almacenan en la base de datos) y desde el código del programa se llaman estos procedimientos con las variables ingresadas por el usuario como los parámetros. De esta forma, el DBMS puede distinguir correctamente variables de código.
Los stored procedures son tan eficaces como los prepared statements, pero hay que tener cuidado de no utilizar los parámetros para armar sentencias, porque estaríamos anulando la defensa.

- Escapar todo dato ingresado por el usuario: probablemente ésta sea la forma más difundida de prevenir SQL Injection y la cual se cita en mucha bibliografía, pero también es la menos recomendada. La idea es que cada vez que el usuario ingrese datos que utilizaremos en una sentencia, escapemos los caracteres especiales (como comillas simples o dobles, barras invertidas "\", o caracteres de comentario "--" o "#", etc) para que el dato sea un solo string y el motor de BD no lo confunda con código a ejecutar. Un ejemplo de función que permite escapar los caracteres especiales, cuando utilizamos php + mysql, es mysql_real_escape_string. Recuerden que esta función no es mágica y se puede evitar como expliqué en el artículo SQL Injection avanzado: consultas simplificadas, file inclusión, ejecución remota y más! en la sección "No puedo usar comillas... no importa!".
Cómo explican en la página de OWASP citada anteriormente, este técnica es frágil y se debe utilizar sólo cuando ya contamos con código inseguro y reescribirlo utilizando prepared statements requeriría un costo inaceptable. Todo desarrollo que se comience de cero debe utilizar prepared statements o stored procedures.


Librería PDO para PHP

Como ya les he mencionado varias veces, soy fan del desarrollo en PHP, por lo cual me expandiré un poco sobre cómo prevenir SQL Injection en este lenguaje. Mi opción favorita para tal caso es utilizar prepared statements, porque mi filosofía es "dejar la base de datos, para almacenar datos", lo cual elimina a los stored procedures como opción. Además de esta forma el código es portable para ser utilizado con distintos motores de base de datos.

En fin, para lograr el objetivo utilizaremos la librería PDO. Como lo describen en la documentación oficial de PHP, PDO (PHP Data Objects) es una interfaz ligera para acceder a bases de datos, la cual permite abstraernos del motor de base de datos, utilizando siempre el mismo conjunto de funciones para acceder a los datos. Esto hace que el código sea portable a cualquiera de las bases de datos soportadas por PDO, entre las que se encuentran las más importantes (MySQL, SQL Server, Postgre, Oracle, SQLite, Informix, Firebird, etc).

PDO no fuerza al usuario a utilizar prepared statements, pero ofrece una serie de funciones para el manejo de los mismos. En ellas se puede utilizar tipado fuerte, lo cual agrega mayor seguridad al especificar el tipo de los datos esperados.


Utilizar los Prepared Statements de PDO

Los prepared statements de PDO son fáciles de utilizar, aunque al principio pueden resultar un poco molestos debido a que se debe escribir un poco más de código al ejecutar consultas.
Veamos una comparación de las funciones que más se utilizan al acceder a la base de datos. Tomo como ejemplo las funciones de MySQL, aunque como saben, PDO funciona con casi cualquier base de datos.

Conexión con la base de datos:
$db = mysq_connect('localhost', 'tester', '123456');
mysql_select_db('test', $db);

Para conectarnos utilizando PDO debemos crear un objeto PDO indicando el tipo de la base de datos a utilizar, el host, el nombre de usuario y el password de la siguiente manera:
$db = new PDO("mysql:host='localhost';dbname='test'", 'tester', '123456');
Consulta a la base de datos:
mysql_query("SELECT * FROM content WHERE id=".$_GET['id'], $db)

Esta es la parte más sensible donde debemos evitar utilizar cualquier parámetro ingresado por el usuario directamente en el string de consulta. Primero preparamos la consulta que debemos ejecutar, indicando con : cuáles son las variables ingresadas por el usuario:
$stmt = $db->prepare("SELECT * FROM content WHERE id= :id");
Luego vinculamos la variable ingresada por el usuario con la utilizada en la consulta, indicamos el tipo, y finalmente ejecutamos la consulta.
$stmt->bindParam(":id", $_GET['id'], PDO::PARAM_INT);
$stmt->execute();
Las 2 llamadas anteriores se pueden unificar en una sola, utilizando un arreglo asociativo en la función execute:
$stmt->execute(array(":id" => $_GET['id']));
La única desventaja de utilizar esta forma de bind es que todos los parámetros son tratados como PDO::PARAM_STR (es decir, strings).
Obtener una fila en un arreglo:
mysql_fetch_array($db);

La forma de obtener filas es muy similar a la llamada anterior, lo que debemos ejecutar es lo siguiente:
$stmt->fetch();
Al igual que mysql_fetch_array, el fetch de PDO obtiene por defecto un arreglo con ambos índices: asociativo y numérico. Si deseamos obtener sólo el arreglo asociativo, podemos utilizar el parámetro PDO::FETCH_ASSOC. En caso de desear uno con índices numéricos el parámetro es PDO::FETCH_NUM:
$stmt->fetch(PDO::FETCH_ASSOC) o $stmt->fetch(PDO::FETCH_NUM);
Además contamos con una función que retorna todas las filas resultantes de la ejecución de la consulta:
$stmt->fetchAll();
Ver errores:
mysql_error($db);

En el caso de prepared statements, los errores se obtienen con errorInfo:
$stmt->errorInfo();
el cual retorna un arreglo que contiene:
0 código de error SQLSTATE
1 código de error
2 mensaje de error

Bueno, con eso cubrimos las funciones básicas de acceso a la base de datos, aunque existen unas cuantas más y las pueden ver en el manual The PDOStatement class.


Conclusión

Prevenir las inyecciones SQL es muy sencillo utilizando prepared statements y es sólo cuestión de ser lo suficientemente responsable (y consciente) a la hora de escribir consultas. Una vez que uno se acostumbra, la escritura del código sale de forma natural y esas "líneas de más" no resultan tan molestas. Recuerden que esto les salvará muchos dolores de cabeza y se sentirán tranquilos de que no los "hackearan" utilizando esta técnica.
Write safely! =)


Referencias

- SQL Injection Prevention Cheat Sheet
- Introduction to PHP PDO
- Prepared statements and stored procedures (Prepared statements and stored procedures)
- PHP manual PDO
- Prepared Statements

5 comentarios:

gilaju dijo...

muy buena explicacion, muchisimas gracias me saco de unas dudas que tenia.

Anónimo dijo...

excelente !!

Anónimo dijo...

Muy buenos post, aportan bastante y estan bien explicados

Anónimo dijo...

Recien me meto en esto de PDO, buena explicación se agredece.

Anónimo dijo...

Hace poco que empecé a utilizar php y creo que es una buena práctica utilizar PDO. Saludos y muy buenos tus posts

Publicar un comentario