Prevención de inyecciones SQL en PHP (y otras vulnerabilidades)
Si ha estado en el desarrollo web durante un tiempo, es casi seguro que ha escuchado el término «inyección SQL» y algunas historias aterradoras al respecto.
PHP, al igual que muchos otros lenguajes, no es inmune a este tipo de amenaza, que puede ser muy peligrosa. Pero, afortunadamente, proteger sus sitios web de la inyección SQL y otras amenazas similares es algo hacia lo que puede tomar medidas tangibles.
En esta publicación, aprenderá qué es la inyección SQL, cuáles son las consecuencias de un ataque exitoso, cómo un atacante puede aprovechar el código PHP vulnerable, qué puede hacer para prevenirlo y qué herramientas puede usar para detectar las partes de su código que podrían estar sujetas a este tipo de amenaza. Además, aprenderá sobre algunas otras vulnerabilidades comunes que debe conocer para crear aplicaciones más seguras.
¿Cómo funciona una inyección SQL?
Lo primero que necesita saber para proteger su código de la inyección SQL es comprender cómo podría ser explotado por un atacante.
La idea detrás del exploit es bastante simple: un atacante ejecuta código SQL malicioso en su base de datos a través de su aplicación.
¿Cómo podría alguien lograrlo? Abusando de las entradas de su aplicación.
veamos un ejemplo sencillo. Supongamos que tiene una aplicación que muestra una lista de nombres de usuario, donde cada usuario tiene un enlace a sus detalles como este:
Y luego, en el archivo user_details.php
, tiene esto:
<?php $sql = "SELECT * FROM user WHERE id = ".$_GET;
Si el usuario actúa como se espera y hace clic en el nombre de algún usuario, la URL de la solicitud se verá así: http://domain.com/user_details.php?id=8
.
Entonces, ¿qué tiene de malo?
El valor de $sql
será la cadena "SELECT * FROM user WHERE id = 8"
y la consulta se ejecutará correctamente.
El problema está en la parte en cursiva de esta oración: Si el usuario actúa como se espera y hace clic en el nombre de algún usuario, la URL de la solicitud se verá así:
http://domain.com/user_details.php?id=8
.
¿Qué pasa si el usuario no actúa como se espera?
¿Qué pasaría si alguien fabricara una URL enviando algo que no sea un número como ID?
Url fabricadas
Si la URL fuera algo así como http://domain.com/user_details.php?id=Peanuts
, la cadena SQL resultante sería: "SELECT * FROM user WHERE id = Peanuts"
.
En este caso, el problema sería para el usuario. La consulta simplemente fallaría y, en el peor de los casos, el usuario vería algún tipo de mensaje extraño.
Pero qué pasaría si la URL se pareciera más a esto:
http://domain.com/user_details.php?id=1+OR+1%3B
? (Este es el resultado de urlencode('1 OR 1;')
, por cierto.)
En este caso, el SQL resultante sería "SELECT * FROM user WHERE id = 1 OR 1;"
. Eso significa que el resultado de la consulta sería cada usuario de la base de datos en lugar de solo el usuario cuyo ID es igual a 1. Ouch.
Pero eso no está tan mal, ¿verdad?
Ahora probemos un ejemplo más extremo.
Supongamos que la URL era algo como
http://domain.com/user_details.php?id=1%27%3B+DROP+TABLE+users%3B--+
, que se traduce en id=1; DROP TABLE users;--
, produciendo efectivamente este comando SQL:
"SELECT * FROM users WHERE id = 1; DROP TABLE users;-- "
. Y si ejecuta esta consulta big gran ouch.
En caso de que no esté 100% familiarizado con la sintaxis SQL, DROP TABLE
eliminaría por completo la tabla. Sería como si nunca hubiera existido.
Por supuesto, una copia de seguridad reciente ayudaría a reducir el daño, pero aún así. Esto es malo.
Si quieres obtener más información sobre este tipo de ataque, hay una gran publicación aquí.
Pero creo que ya son suficientes malas noticias. Pasemos a un lugar más alegre: cómo puede evitar este tipo de inyecciones SQL en sus aplicaciones PHP.
Cómo evitar las inyecciones SQL en PHP
Como ha visto, el problema es cuando un atacante tiene la capacidad de ejecutar código SQL sin que usted lo sepa.
Esta situación se presenta como consecuencia de tener consultas creadas sobre la marcha a través de la concatenación de cadenas e incluir datos recopilados de entradas externas (generalmente entrada de usuario, pero también otras fuentes externas como archivos, respuestas a llamadas a API, etc.).
Por lo tanto, para evitar esta vulnerabilidad en su código, debe corregir las fuentes de los problemas potenciales.
Valide sus entradas
Una forma sencilla de corregir la vulnerabilidad en el ejemplo anterior sería comprobar si el parámetro ID es lo que realmente se espera de él (p. ej., un entero positivo):
Otra forma de hacer este tipo de validación es aprovechar los filtros integrados de PHP:
<?php
$id = filter_input( INPUT_GET, 'id', FILTER_VALIDATE_INT);
Por lo tanto, al validar sus entradas, evita que los atacantes ejecuten código malicioso junto con el suyo.
No hay una manera fácil de evitar que lo intenten, pero mientras sus intentos no tengan éxito, ya está listo.
Use instrucciones preparadas
Otra forma de proteger su código contra inyecciones SQL es mediante el uso de instrucciones preparadas. Las instrucciones preparadas son comandos SQL precompilados.
Se pueden usar con una biblioteca de acceso a bases de datos específica (como mysqli) o con la biblioteca más genérica PDO.
Echemos un vistazo a un ejemplo usando mysqli:
Si prueba este código (reemplazando las credenciales de la base de datos, por supuesto), verá que, si se accede a través de una URL legal como http://domain.com/user_details.php?id=1
, obtendrá el mismo resultado que cualquiera de las URL maliciosas anteriores (que es la información sobre el usuario asociada con ID 1 y nada más).
Si prefiere usar DOP, el código se verá más como esto:
No hay mucha diferencia, ¿verdad?
En resumen, las instrucciones preparadas ofrecen una excelente manera de prevenir las inyecciones SQL. Además, por lo general, funcionan mejor que SQL sobre la marcha.
Ahora que conoce la herramienta, solo necesita usarla.
Cómo detectar código PHP que es vulnerable a inyecciones SQL
Una vez que se encuentra la vulnerabilidad, arreglarla no es realmente complicado. Pero, ¿cómo encuentras este tipo de vulnerabilidades en tu código en primer lugar?
Bueno, si tienes suerte y tu base de código es lo suficientemente pequeña, basta con una simple revisión del código.
Pero si su aplicación es de tamaño mediano a grande, necesitará ayuda adicional.
Una herramienta muy popular (y de código abierto) que puede usar para este propósito es sqlmap, un sencillo script de Python que intentará atacar cualquier URL que le proporcione e informará de sus hallazgos.
Aquí hay una salida de muestra de una ejecución en un sitio web mío basado en PHP:
Otra herramienta de código abierto que puedes usar es Arachni, una herramienta más sofisticada que incluye una interfaz gráfica de usuario web y fue escrita en Ruby.
Aquí hay una salida de muestra de ejecutarla a través de la CLI:
Más allá de la revisión y las pruebas de código, debe considerar el uso de una solución de escofina, como Sqreen, para monitorear sus entornos de producción y detener la ejecución de cualquier exploit que supere las etapas de revisión y prueba.
¿Qué otras vulnerabilidades debe tener en cuenta?
Inyecciones de código
La inyección SQL es solo un miembro de una familia más grande de vulnerabilidades: inyección de código.
La idea detrás de diferentes técnicas de inyección de código es siempre la misma. Se trata de engañar a una aplicación inocente para que ejecute código malicioso.
Esto se puede hacer de varias maneras.
En PHP, hay funciones diseñadas para simplemente emitir comandos directamente en el sistema operativo, como la función exec
.
he Aquí un ejemplo simple de cómo esto podría ser explotado:
<?php
exec( 'cp uploads/'.$_GET.' /secure/');
Si alguien fuera a pedir http://domain.com/exec.php?file=a.txt%3B+rm+-Rf+%2A+%23
, el resultado sería la ejecución de este comando:
cp uploads/a.txt; rm -Rf * # /secure/
Y esto podría borrar todo el disco en el servidor—no validados entrada ataca de nuevo!
Cross-site scripting
Otra vulnerabilidad muy común es cross-site scripting (XSS). En este caso, la víctima es el usuario que visita su sitio.
XSS es cuando un atacante inyecta código JavaScript malicioso dentro de un formulario HTML normal, que luego será renderizado por el navegador de otro usuario.
Mira este script PHP:
<?php
echo "<html><p>Hello {$_GET}!</p></html>";
Claramente, la intención es crear una página simple que recibe el usuario.
Pero cuando los malos llegan a él, lo que les impide solicitar
http://domain.com/greet.php?name=Mauro%3Cscript+language%3D%22javascript%22%3Ealert%28%22You+are+hacked%21%22%29%3B%3C%2Fscript%3E
? Esta solicitud producirá el siguiente HTML como salida:
<html><p>Hello Mauro<script language="javascript">alert("You are hacked!");</script></p></html>
Esto es claramente una hazaña muy ingenua, después de todo, ¿cuál es el daño en mostrar a alguien un simple «¡Estás hackeado!»mensaje? Pero las cosas pueden ponerse serias con un poco de imaginación. Solo tenga en cuenta que cada solicitud hecha a un servidor incluye la cookie de sesión, por lo que esta vulnerabilidad puede ser la culpable detrás de que las sesiones de sus usuarios sean secuestradas.
Por otra parte, aunque los problemas se ven feos, las correcciones son realmente simples: Todo se trata de validación y desinfección.
En el caso de XSS, bastará una simple llamada a htmlentities
antes de producir la salida real.
Corregir vs detectar vulnerabilidades de seguridad
Como vio en esta publicación, reparar vulnerabilidades de seguridad, tanto inyecciones SQL como de otros tipos, no es el principal desafío. Darse cuenta de que su código es vulnerable, y donde puede ser explotado, lo es. Tienes un par de opciones aquí:
- Tenga una disciplina de codificación muy sólida
- Pruebe sus aplicaciones a fondo para detectar problemas de seguridad
- Aproveche las herramientas de supervisión y protección para evitar vulnerabilidades y recibir alertas de manera oportuna
Lo importante es prestar atención a estos problemas y actuar sobre la base de ese conocimiento. Mantente a salvo.