¿Qué es la inyección SQL y cómo prevenirla en aplicaciones PHP?

Entonces, ¿piensa que su base de datos SQL tiene un buen rendimiento y está a salvo de una destrucción instantánea? Bueno, SQL Injection no está de acuerdo.

Sí, estamos hablando de destrucción instantánea, porque no quiero abrir este artículo con la habitual terminología poco convincente de «reforzar la seguridad» y «prevenir el acceso malicioso». SQL Injection es un truco tan antiguo en el libro que todos, todos los desarrolladores, lo conocen muy bien y saben cómo prevenirlo. Excepto por ese momento extraño en el que se equivocan, y los resultados pueden ser nada menos que desastrosos.

Si ya sabe qué es la inyección SQL, no dude en pasar a la segunda mitad del artículo. Pero para aquellos que recién comienzan en el campo del desarrollo web y sueñan con asumir roles más importantes, es necesaria una introducción.

¿Qué es la inyección SQL?

La clave para entender la Inyección SQL está en su nombre: SQL + Inyección. La palabra «inyección» aquí no tiene connotaciones médicas, sino que es el uso del verbo «inyectar». Juntas, estas dos palabras transmiten la idea de poner SQL en una aplicación web.

Poner SQL en una aplicación web. . . mmm . . . ¿No es eso lo que estamos haciendo de todos modos? Sí, pero no queremos que un atacante controle nuestra base de datos. Entendamos eso con la ayuda de un ejemplo.

Supongamos que está creando un sitio web PHP típico para una tienda de comercio electrónico local, por lo que decide agregar un formulario de contacto como este:

<form action="record_message.php" method="POST">
  <label>Your name</label>
  <input type="text" name="name">
  
  <label>Your message</label>
  <textarea name="message" rows="5"></textarea>
  
  <input type="submit" value="Send">
</form>

Y supongamos que el archivo send_message.php almacena todo en una base de datos para que los propietarios de la tienda puedan leer los mensajes de los usuarios más adelante. Puede tener algún código como este:

<?php

$name = $_POST['name'];
$message = $_POST['message'];

// check if this user already has a message
mysqli_query($conn, "SELECT * from messages where name = $name");

// Other code here

Entonces, primero está tratando de ver si este usuario ya tiene un mensaje sin leer. La consulta SELECCIONAR * de mensajes donde nombre = $nombre parece bastante simple, ¿verdad?

¡EQUIVOCADO!

En nuestra inocencia, hemos abierto las puertas a la destrucción instantánea de nuestra base de datos. Para que esto suceda, el atacante debe cumplir las siguientes condiciones:

  • La aplicación se ejecuta en una base de datos SQL (hoy en día, casi todas las aplicaciones lo hacen)
  • La conexión de base de datos actual tiene permisos de «edición» y «eliminación» en la base de datos
  • Los nombres de las tablas importantes se pueden adivinar

El tercer punto significa que ahora que el atacante sabe que está ejecutando una tienda de comercio electrónico, es muy probable que esté almacenando los datos del pedido en una tabla de pedidos. Armado con todo esto, todo lo que el atacante debe hacer es proporcionar esto como su nombre:

José; truncar órdenes;? ¡Sí, señor! Veamos en qué se convertirá la consulta cuando sea ejecutada por el script PHP:

SELECT * FROM mensajes DONDE nombre = Joe; truncar órdenes;

De acuerdo, la primera parte de la consulta tiene un error de sintaxis (sin comillas alrededor de «Joe»), pero el punto y coma obliga al motor de MySQL a comenzar a interpretar uno nuevo: órdenes truncadas. ¡Así de simple, de un solo golpe, todo el historial de pedidos desaparece!

Ahora que sabe cómo funciona la inyección de SQL, es hora de ver cómo detenerla. Las dos condiciones que deben cumplirse para una inyección SQL exitosa son:

  • El script PHP debe tener privilegios de modificación/eliminación en la base de datos. Creo que esto es cierto para todas las aplicaciones y no podrá hacer que sus aplicaciones sean de solo lectura. 🙂 Y adivine qué, incluso si eliminamos todos los privilegios de modificación, la inyección de SQL aún puede permitir que alguien ejecute consultas SELECT y vea toda la base de datos, incluidos los datos confidenciales. En otras palabras, reducir el nivel de acceso a la base de datos no funciona y su aplicación lo necesita de todos modos.
  • La entrada del usuario se está procesando. La única forma en que puede funcionar la inyección de SQL es cuando acepta datos de los usuarios. Una vez más, no es práctico detener todas las entradas de su aplicación solo porque le preocupa la inyección SQL.
  • Prevención de la inyección de SQL en PHP

    Ahora, dado que las conexiones de bases de datos, las consultas y las entradas de los usuarios son parte de la vida, ¿cómo evitamos la inyección de SQL? Afortunadamente, es bastante simple y hay dos formas de hacerlo: 1) desinfectar la entrada del usuario y 2) usar declaraciones preparadas.

    Desinfectar la entrada del usuario

    Si está utilizando una versión anterior de PHP (5.5 o inferior, y esto sucede mucho en el alojamiento compartido), es aconsejable ejecutar todas las entradas de usuario a través de una función llamada mysql_real_escape_string(). Básicamente, lo que hace es eliminar todos los caracteres especiales en una cadena para que pierdan su significado cuando los usa la base de datos.

    Por ejemplo, si tiene una cadena como Soy una cadena, un atacante puede usar el carácter de comilla simple (‘) para manipular la consulta de la base de datos que se está creando y causar una inyección SQL. Ejecutarlo a través de mysql_real_escape_string() produce Soy una cadena, que agrega una barra invertida a la comilla simple, escapándola. Como resultado, la cadena completa ahora pasa como una cadena inofensiva a la base de datos, en lugar de poder participar en la manipulación de consultas.

    Hay un inconveniente con este enfoque: es una técnica muy, muy antigua que va junto con las formas más antiguas de acceso a la base de datos en PHP. A partir de PHP 7, esta función ya ni siquiera existe, lo que nos lleva a nuestra próxima solución.

    Usar declaraciones preparadas

    Las declaraciones preparadas son una forma de hacer que las consultas a la base de datos sean más seguras y confiables. La idea es que en lugar de enviar la consulta sin procesar a la base de datos, primero le decimos a la base de datos la estructura de la consulta que enviaremos. Esto es lo que entendemos por “preparar” una declaración. Una vez que se prepara una declaración, pasamos la información como entradas parametrizadas para que la base de datos pueda «llenar los vacíos» conectando las entradas a la estructura de consulta que enviamos antes. Esto elimina cualquier poder especial que puedan tener las entradas, lo que hace que se las trate como meras variables (o cargas útiles, por así decirlo) en todo el proceso. Así es como se ven las declaraciones preparadas:

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";
    
    // Create connection
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Check connection
    if ($conn->connect_error) {
        die("Connection failed: " . $conn->connect_error);
    }
    
    // prepare and bind
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);
    
    // set parameters and execute
    $firstname = "John";
    $lastname = "Doe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Mary";
    $lastname = "Moe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Julie";
    $lastname = "Dooley";
    $email = "[email protected]";
    $stmt->execute();
    
    echo "New records created successfully";
    
    $stmt->close();
    $conn->close();
    ?>

    Sé que el proceso suena innecesariamente complejo si eres nuevo en las declaraciones preparadas, pero el concepto bien vale la pena. Aquí está una buena introducción a la misma.

    Para aquellos que ya están familiarizados con la extensión PDO de PHP y la usan para crear declaraciones preparadas, tengo un pequeño consejo.

    Advertencia: tenga cuidado al configurar PDO

    Cuando usamos PDO para acceder a la base de datos, podemos dejarnos atrapar por una falsa sensación de seguridad. “Ah, bueno, estoy usando PDO. Ahora no necesito pensar en nada más”, así es como funciona nuestro pensamiento en general. Es cierto que PDO (o declaraciones preparadas de MySQLi) es suficiente para prevenir todo tipo de ataques de inyección SQL, pero debe tener cuidado al configurarlo. Es común simplemente copiar y pegar el código de los tutoriales o de sus proyectos anteriores y continuar, pero esta configuración puede deshacer todo:

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

    Lo que hace esta configuración es decirle a PDO que emule declaraciones preparadas en lugar de usar la función de declaraciones preparadas de la base de datos. En consecuencia, PHP envía cadenas de consulta simples a la base de datos incluso si su código parece estar creando declaraciones preparadas y configurando parámetros y todo eso. En otras palabras, eres tan vulnerable a la inyección SQL como antes. 🙂

    La solución es simple: asegúrese de que esta emulación esté configurada en falso.

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

    Ahora, el script PHP se ve obligado a usar declaraciones preparadas a nivel de base de datos, lo que evita todo tipo de inyección SQL.

    Prevención del uso de WAF

    ¿Sabe que también puede proteger las aplicaciones web de la inyección SQL utilizando WAF (firewall de aplicaciones web)?

    Bueno, no solo la inyección SQL, sino muchas otras vulnerabilidades de la capa 7, como secuencias de comandos entre sitios, autenticación rota, falsificación entre sitios, exposición de datos, etc. Puede usar alojamiento propio como Mod Security o basado en la nube como se indica a continuación.

    Inyección SQL y frameworks PHP modernos

    La inyección de SQL es tan común, tan fácil, tan frustrante y tan peligrosa que todos los marcos web PHP modernos vienen integrados con contramedidas. En WordPress, por ejemplo, tenemos la función $wpdb->prepare(), mientras que si está utilizando un marco MVC, hace todo el trabajo sucio por usted y ni siquiera tiene que pensar en evitar la inyección de SQL. Es un poco molesto que en WordPress tengas que preparar declaraciones explícitamente, pero bueno, estamos hablando de WordPress. 🙂

    De todos modos, mi punto es que la raza moderna de desarrolladores web no tiene que pensar en la inyección de SQL y, como resultado, ni siquiera son conscientes de la posibilidad. Como tal, incluso si dejan una puerta trasera abierta en su aplicación (tal vez sea un parámetro de consulta $_GET y los viejos hábitos de activar una consulta sucia), los resultados pueden ser catastróficos. Por lo tanto, siempre es mejor tomarse el tiempo para profundizar en los cimientos.

    Conclusión

    SQL Injection es un ataque muy desagradable en una aplicación web, pero se evita fácilmente. Como vimos en este artículo, tener cuidado al procesar la entrada del usuario (por cierto, SQL Injection no es la única amenaza que trae el manejo de la entrada del usuario) y consultar la base de datos es todo lo que hay que hacer. Dicho esto, no siempre estamos trabajando en la seguridad de un framework web, por lo que es mejor estar atento a este tipo de ataques y no caer en ellos.