Inyección SQL

Categories:

Una inyección de SQL es una vulnerabilidad de las aplicaciones web en la cual el usuario que utiliza nuestra aplicación envía una sentencia de código SQL a nuestra aplicación. Esta consulta es interpretada por el administrador de base de datos realizando tareas que no estaban contempladas en la consulta original.

¿Cómo sucede esto?

La manera más directa es utilizar cualquier campo de texto de formulario. En lugar de escribir el valor que tenemos contemplado para ese campo – nombre, correo, término de búsqueda, etc. – se introduce texto que se interpreta como una sentencia SQL.
Si el valor del texto recibido se utiliza directamente en la consulta que vamos a enviar al administrador de la base de datos, éste la interpreta como una consulta válida.

El ejemplo más común es en las búsquedas:

# Obtenemos el valor enviado por el usuario
  $q = $_GET['query'];
  # Creamos la consulta utilizando el valor enviado por el usuario
  $query = "SELECT * FROM usuarios WHERE nombre = '" . $q . "'";
  # Mandamos la consulta directamente al administrador de la base de datos.
  $results = $conexion->query($query);
  
  # Si el valor enviado por el usuario es admin, la sentencia quedaría:

    SELECT * FROM usuarios WHERE nombre = 'admin';

Recordemos que en MySQL podemos agregar más condiciones a las consultas.

En este caso vamos a utilizar OR, si recordamos para que se utiliza:

“OR se utiliza cuando se quiere que cualquiera de las condiciones utilizadas se cumpla.
Los registros que cumplan con alguna de las dos condiciones – o con las dos condiciones – son tomados en cuenta.”

Siguiendo con el ejemplo anterior – considerando que están buscando al usuario con nombre admin -.

SELECT * FROM usuarios WHERE nombre = 'admin' OR <otra-condición>

Las preguntas que surgen en este momento son:

¿Cuál es la condición que el usuario quiere ejecutar para que sea válida su consulta?
¿Cómo sabe, deduce o infiere el usuario el nombre de mis columnas?

En este caso el usuario no necesita tener conocimiento de la estructura – esquema – de nuestra(s) tabla(s). La condición más simple que quiere que se cumpla es la siguiente:

# ¿Uno es igual a uno?
  1 = 1
  # Si....

El ejemplo más usado es 1=1.
Al manejador de la base de datos le llegaría la sentencia siguiente:

SELECT * FROM usuarios WHERE nombre = 'admin' OR 1=1
  
  
# Selecciona todas las columnas de la tabla usuarios
# donde el valor en nombre tiene que ser igual a admin
# o donde 1 = 1

La sentencia está bien formada, no generaría error y sería evaluada.

1=1 se interpreta como verdadero y de esta manera se obtienen todos los datos de los usuarios.

Si regresamos a la definición de nuestra consulta en PHP vemos que hay comillas sencillas que encierran el término de búsqueda – dentro de las comillas dobles -.

Este aspecto es relativamente fácil de intuir, entonces, el término de búsqueda puede incluir comillas sencillas, dobles, codificación con entidades HTML, etc. En este punto se trata de probar una combinación y revisar la respuesta que envía la aplicación web.

# Comillas sencillas / dobles 
# sin utilizar un término de búsqueda
# ' OR '1=1
# " OR "1=1

# Comillas sencillas / dobles 
# utilizando un término de búsqueda
 # admin' OR '1=1
 # admin" OR "1=1
  
# Incluso funciona para búsquedas
# que utilizan el operador LIKE

# Definida
$query = "SELECT * FROM usuarios WHERE nombre LIKE '%" . $q . "%'";

# Interpretada
 $query = "SELECT * FROM usuarios WHERE nombre LIKE '%admin' OR '1=1%'";  

Sentencias SQL preparadas

Esta es la manera en que podemos mitigar los ataques de inyección SQL. Al definir nuestras sentencias de esta manera, le indicamos al administrador de la base de datos el tipo de valor que va a contener cada uno de los parámetros utilizados en una consulta. El administrador lo va a interpretar el contenido como el tipo de dato especificado pero no como código válido SQL.

¿Cómo se prepara una sentencia SQL?

Este proceso se realiza en tres pasos.

  1. Preparar la sentencia.
    $query = $conexion->prepare("SELECT * FROM usuarios WHERE nombre = ?");
  2. Enlazar los parámetros *.
    $query->bind_param('s', $q);
  3. Ejecutar la consulta.
    $query->execute();
  4. Obtener resultados de la consulta – opcional -.
    $results =  $query->get_result();

* La s se refiere a que el valor que está en la variable $q es una cadena de texto – string -. Los tipos de datos permitidos son – https://www.php.net/manual/es/mysqli-stmt.bind-param – :

Carácter Descripción
i Entero – integer –
d Decimal – double –
s Cadena de Texto – string –
b Blob – Binary Large Object – y se envía en paquetes

Un ejemplo de cómo se visualizaría una sentencia preparada.

<?php
  $conexion = new mysqli("127.0.0.1", "root", "", "tutoriales");
  $conexion->query("SET NAMES utf8mb4");
  $conexion->query("SET CHARACTER SET utf8mb4");

  # Por el momento asignamos directamente el valor del parámetro 'termino' - sería término -
  # a la variable $q
  $q = $_GET['termino'];  
  
  # Se utiliza el signo de cierre de interrogación para indicar que ahí va el valor que 
  # queremos sustituir. No se utilizan comillas alrededor de este caracter
  $query = $conexion->prepare("SELECT * FROM usuarios WHERE nombre = ?");
  
  # Asociamos el parámetro definido en la sentencia - ? - con la variable $q
  # Indicamos que el valor lo va a interpretar como cadena de texto - s -  
  $query->bind_param('s', $q);
  
  # Ejecutamos la sentencia
  $query->execute();
  
  # Obtenemos los resultados
  $results =  $query->get_result();

  while ($result = $results->fetch_array()) {
    echo $result['nombre'] . "<br>";
  }
  
?>
<?php
  $conexion = new mysqli("127.0.0.1", "root", "", "tutoriales");
  $conexion->query("SET NAMES utf8mb4");
  $conexion->query("SET CHARACTER SET utf8mb4");

  # A diferencia del ejemplo anterior, en este caso vamos a concatenar dos signos de porcentaje  
  # alrededor del parámetro término. Esto es porque vamos a utilizar el operador de comparación 
  # LIKE 
  $q = "%" . $_GET['termino'] . "%";
  
  # Se utiliza el signo de cierre de interrogación para indicar que ahí va el valor que 
  # queremos sustituir. No se utilizan comillas alrededor de este caracter
  # En este caso los comodines - % % - se asocian al término de búsqueda en lugar
  # de ser parte de la consulta
  $query = $conexion->prepare("SELECT * FROM usuarios WHERE nombre LIKE ?");
  
  # Asociamos el parámetro definido en la sentencia - ? - con la variable $q
  # Indicamos que el valor lo va a interpretar como cadena de texto - s -  
  $query->bind_param('s', $q);
  
  # Ejecutamos la sentencia
  $query->execute();
  
  # Obtenemos los resultados
  $results =  $query->get_result();

  while ($result = $results->fetch_array()) {
    echo $result['nombre'] . "<br>";
  }
  
?>

Si utilizamos un término de búsqueda como el siguiente – en ambas consultas – no se devuelve ningún resultado.

# admin' OR '1=1

Esto quiere decir que el contenido se interpreto como texto, no como una sentencia SQL válida y da a entender que en la tabla no existe un registro que contenga ese término en la columna nombre.

Al utilizar sentencias preparadas el código escrito es mayor en comparación de las consultas directas pero las consultas que ejecutemos van a ser un poco más seguras.