Prevenir ataques mediante inyección SQL (SQL injection) en PHP

Prevenir ataques mediante inyección SQL (SQL injection) en PHP

Como desarrollador web freelance llevo lidiando más de una década con los ataques a las páginas. Existe multitud de modos de romper la seguridad de un sitio, sin embargo, curiosamente la inyección SQL o SQL injection sigue estando arriba en el ranking de métodos más usado a pesar de ser una vulnerabilidad descubierta hace ya más de 20 años.

Por este motivo se me ha ocurrido hacer un breve artículo con diversas prácticas que puedes poner en marcha para prevenir en la medida de lo posible este tipo de situaciones. Si bien la seguridad total no existe y al final en gran medida abrir una brecha de seguridad depende de la insistencia del atacante, no menos cierto es que como en el caso de cualquier tipo de prevención de malware, cuantas más medidas puedas adoptar, más seguridad tendrás.

¿Qué es la inyección SQL?

SQL injection es una técnica que permite explotar vulnerabilidades en la seguridad de aplicaciones web que interactúan con bases de datos a través de consultas SQL. Este tipo de sistema permite a un atacante ejecutar consultas SQL maliciosas en la base de datos, aprovechando una vulnerabilidad en las partes que permiten una entrada de datos por parte del usuario. Normalmente se suelen encontrar en los formularios y rutas.

Cuando un programador construye consultas SQL concatenando directamente las entradas del usuario en la cadena de consulta, sin validar ni sanitizar adecuadamente estos datos, se crea una abertura que permite inyectar SQL personalizado. De este modo, el atacante puede manipular las entradas de tal manera que la consulta SQL original se vea alterada, permitiendo la ejecución de comandos no autorizados.

Las consecuencias de este acceso no autorizado puede ser muy dispar, pero básicamente es que el atacante tendrá acceso parcial o total a la base de datos de la web. Puede acceder a datos sensible, manipular contenidos, cambiar contraseñas e incluso eliminar las tablas. Todo dependerá del tipo de acceso que adquiera y por qué no decirlo, de la mala leche del individuo en cuestión.

Técnicas de prevención

Existen diversas técnicas que nos pueden ayudar a prevenir las inyecciones SQL en PHP. Aquí vamos a ver algunas de ellas como puede ser el uso de sentencias preparadas, la validación en las entradas de usuario, el uso de escapado de datos, las listas blancas o el control de permisos. Todo esto nos ayudarán a subir un peldaño más la seguridad del sitio web en el que estemos programando.

Sentencias preparadas

En lugar de concatenar directamente los datos del usuario en la cadena de consulta, las sentencias preparadas permiten la creación de consultas con marcadores de posición, que se llenan con datos proporcionados posteriormente.

Las sentencias preparadas constan de dos partes: la consulta SQL con marcadores de posición y los valores reales que se asignarán a esos marcadores. El motor de la base de datos se encargará de tratar los valores de manera segura, evitando así la posibilidad de inyección de SQL. Un ejemplo usando la extensión para PHP MySQLi podría ser:

// Entradas del usuario
$username = $_POST['username'];
$password = $_POST['password'];

// Sentencia preparada
$stmt = $mysqli->prepare("SELECT * FROM usuarios WHERE nombre_usuario = ? AND clave = ?");
$stmt->bind_param("ss", $username, $password);

Al utilizar marcadores de posición, las sentencias preparadas eliminan la posibilidad de que los datos del usuario alteren la estructura de la consulta. Además puedes reutilizar la misma sentencia preparada con diferentes conjuntos de datos, lo que mejora el rendimiento y reduce la complejidad del código.

Validación de datos del usuario

La validación de entradas es esencial para evitar situaciones no deseadas. Aparte de limitar la inyección de código malicioso, ayuda a prevenir datos incorrectos o errores en la aplicación al garantizar que los datos introducidos cumplan con los criterios que hemos definido y que se esperan. Por ejemplo, si tenemos un campo en un formulario para el teléfono, debemos asegurarnos que sólo se pueden pasar números.

Hay dos vías de hacer la validación: del lado del cliente, es decir del navegador, y del lado del servidor. Lo ideal es implementar ambas, ya que hacen las veces de filtro doble que ofrecen una mayor protección. Veamos rápidamente algunas ideas de cómo llevarlo a cabo en ambos casos, ya que debes tener en cuenta que no hay un único modo de hacerlo.

Validación del lado del cliente

Tradicionalmente en programación web se ha venido utilizando casi siempre JavaScript para esta tarea. El modo de actuar es crear una función que revise paso a paso los datos que el usuario ha introducido. En el caso de un formulario, actualmente HTML5 dispone de tipos de input concretos que nos ayudan a validar datos sin necesidad de hacer nada más.

No obstante, para cuando no es posible usar la validación por defecto o simplemente cuando queremos personalizarla, podemos hacerlo cuando el usuario pulse en el botón de enviar. En ese punto, en lugar de enviarlo directamente, revisamos en el navegador del cliente que estén correctos.

Supongamos que tenemos un formulario con un campo para la edad, que debe ser introducida con dígitos. Al revisar el valor dentro de la función, un ejemplo sencillo podría ser comprobar que es un número entero positivo, ya que nadie puede tener la edad en negativo y con decimales.


<form id="edadForm" onsubmit="return validarEdad()">
    <label for="edad">Edad:</label>
    <input type="number" id="edad" name="edad">
    <span id="edadError" class="error"></span>
    <input type="submit" value="Enviar">
</form>

<script>
function validarEdad() {
    // Obtener el valor del campo de edad
    var edad = document.getElementById("edad").value;

    // Validar que sea un número entero positivo
    if (!/^[\d]+$/.test(edad) || parseInt(edad) <= 0) {
        document.getElementById("edadError").innerHTML = "La edad debe ser un número entero positivo.";
        return false; // Evitar que se envíe el formulario
    }

    // Restablecer el mensaje de error si la validación es exitosa
    document.getElementById("edadError").innerHTML = "";

    // Continuar con el envío del formulario
    return true;
}
</script>

Si la condición se cumple, el formulario continuará con el envío. Y aquí es donde entraría la siguiente vía de validación.

Validación del lado del servidor

La validación del lado del servidor es crucial, ya que los datos del lado del cliente pueden ser manipulados desde el DOM, por lo que si bien es una primera capa de seguridad, tampoco es suficiente. El método a seguir sería similar a como lo hemos hecho antes, pero esta vez usando PHP. De modo que el usuario manda los datos, que se validan primero en navegador y luego pasan a una segunda validación en el servidor antes de mandarlos definitivamente.

Siguiendo el ejemplo anterior, veamos cómo se puede validar un campo destinado a introducir la edad pero del lado del servidor usando PHP.

<?php
// Función para validar la edad
function validarEdad($edad) {
    // Validar que sea un número entero positivo
    if (!ctype_digit($edad) || intval($edad) <= 0) {
        return "La edad debe ser un número entero positivo.";
    }

    // Restablecer el mensaje de error si la validación es exitosa
    return "";
}

// Manejar el envío del formulario
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    // Obtener el valor del campo de edad
    $edad = $_POST["edad"];

    // Validar la edad
    $errorEdad = validarEdad($edad);

    // Mostrar el mensaje de error si existe
    if ($errorEdad !== "") {
        echo '<div class="error">' . $errorEdad . '</div>';
    } else {
        // La validación es exitosa, continuar con el procesamiento
        echo "Edad válida: " . $edad;
    }
}
?>

<h1>Formulario con Validación de Edad</h1>

<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
    <label for="edad">Edad:</label>
    <input type="number" id="edad" name="edad">
    <br>
    <input type="submit" value="Enviar">
</form>

Escapado de datos

Consiste en procesar los datos antes de usarlos en consultas SQL para evitar que los caracteres especiales sean interpretados como parte de la sintaxis SQL. Cuando los datos ingresados por un usuario se utilizan directamente en una consulta SQL sin ser escapados, existe el riesgo de que un atacante pueda manipular la consulta insertando comandos SQL maliciosos. El escapado de datos ayuda a prevenir este tipo de ataques al asegurarse de que los datos sean tratados como datos y no como parte de la sintaxis SQL.

Si bien podemos crear nuestra propia función para hacer esto, la mayoría de las extensiones de bases de datos en PHP proporcionan funciones específicas para escapar datos de manera segura antes de usarlos en consultas SQL. Por ejemplo, mysqli tiene la función mysqli_real_escape_string, y PDO tiene la función PDO::quote.

// Entradas del usuario
$nombre = $_POST['nombre'];

// Escapar los datos antes de usarlos en la consulta
$nombreEscapado = $mysqli->real_escape_string($nombre);

// Construir y ejecutar la consulta utilizando los datos escapados
$query = "INSERT INTO usuarios (nombre) VALUES ('$nombreEscapado')";
$resultado = $mysqli->query($query);

Realizar el escapado de datos de manera conjunta con las sentencias preparadas, como vimos antes, ayuda a proteger considerablemente una web ante el intento de inyección SQL.

Lista blanca

El uso de una lista blanca, también conocido como whitelisting en inglés, es una técnica de seguridad que consiste en permitir solo ciertos elementos o valores conocidos y previamente autorizados, mientras se rechazan todos los demás. Esta estrategia se reduce significativamente el riesgo de que se introduzcan datos que puedan causar daño o comprometer la seguridad de la aplicación.

Un ejemplo muy básico de implementar esto podría ser contar con una lista blanca de nombres para aceptar el login en una aplicación sólo a ciertos usuarios que conocemos previamente. Esto se podría extender a cualquier otro valor que consideremos seguro.

Sin embargo, su aplicación no tiene por qué limitarse únicamente a la definición de valores permitidos, sino que también se extiende a la restricción de acceso, ejecución o uso de recursos a un conjunto específico de elementos previamente identificados como seguros y confiables.

Control de permisos

Garantizar que solo los usuarios autorizados tengan acceso a los datos y que estos usuarios tengan los permisos adecuados es fundamental para prevenir accesos no autorizados y manipulaciones maliciosas de los datos en la base de datos. Las restricciones de privilegios ayudan a limitar el impacto de posibles vulnerabilidades y a proteger la seguridad de los datos sensibles.

-- Otorgamos al usuario permisos de solo lectura en la tabla indicada
GRANT SELECT ON basededatos.tabla TO 'usuario'@'localhost';

Es buena idea crear roles de usuario concretos con diferentes permisos, de este modo podrás controlar qué permites hacer a cada uno. La premisa debería ser siempre permitir a cada usuario hacer sólo lo que necesita y nada más.


Referencias:

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *