Protección frente a Inyección SQL

Avanzado
PHP
PHP
Actualizado: 17/02/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

Uso estricto de sentencias preparadas (Prepared Statements)

Una mejora fundamental en la protección frente a inyecciones SQL es el uso estricto de sentencias preparadas. Las sentencias preparadas permiten separar la lógica SQL de los datos proporcionados por el usuario, evitando que entradas maliciosas modifiquen la estructura de la consulta.

En PHP, podemos utilizar PDO (PHP Data Objects) o MySQLi para implementar sentencias preparadas de manera eficiente. Tanto PDO como MySQLi proporcionan métodos para preparar consultas y enlazar parámetros, garantizando que los datos sean tratados correctamente.

A continuación, se presenta un ejemplo usando PDO:

<?php
$dsn = 'mysql:host=localhost;dbname=mi_base_de_datos';
$usuario = 'mi_usuario';
$contraseña = 'mi_contraseña';

try {
    $conexion = new PDO($dsn, $usuario, $contraseña);
    $conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $sql = 'SELECT * FROM usuarios WHERE email = :email';
    $stmt = $conexion->prepare($sql);
    $emailUsuario = $_POST['email'];

    $stmt->bindParam(':email', $emailUsuario, PDO::PARAM_STR);
    $stmt->execute();

    $resultados = $stmt->fetchAll(PDO::FETCH_ASSOC);
    foreach ($resultados as $fila) {
        echo 'Usuario: ' . $fila['nombre'] . "\n";
    }
} catch (PDOException $e) {
    echo 'Error de conexión: ' . $e->getMessage() . "\n";
}

En este ejemplo, se utiliza $conexion->prepare() para preparar la sentencia SQL y $stmt->bindParam() para enlazar el parámetro :email al valor proporcionado por el usuario. De esta manera, se evita la inserción de código SQL malicioso.

Es importante destacar que el uso de sentencias preparadas no solo mejora la seguridad, sino también el rendimiento en consultas repetitivas. Al preparar una sentencia una vez, el servidor de bases de datos puede optimizar la ejecución de esa consulta para usos posteriores.

Si preferimos utilizar MySQLi, el enfoque es similar:

<?php
$conexion = new mysqli('localhost', 'mi_usuario', 'mi_contraseña', 'mi_base_de_datos');

if ($conexion->connect_error) {
    die("Error de conexión: $conexion->connect_error\n");
}

$sql = 'SELECT * FROM usuarios WHERE email = ?';
$stmt = $conexion->prepare($sql);

$emailUsuario = $_POST['email'];
$stmt->bind_param('s', $emailUsuario);
$stmt->execute();

$resultado = $stmt->get_result();
while ($fila = $resultado->fetch_assoc()) {
    echo 'Usuario: ' . $fila['nombre'] . "\n";
}

$stmt->close();
$conexion->close();

Aquí, usamos $conexion->prepare() para preparar la consulta y $stmt->bind_param() para enlazar el parámetro. La s en bind_param('s', $emailUsuario) indica que el parámetro es una cadena (string).

Es crucial utilizar sentencias preparadas siempre que se trabaje con datos proporcionados por usuarios, incluso si se considera que los datos son confiables. Esto incluye entradas de formularios, variables de URL y cualquier otra fuente externa.

Además, al utilizar sentencias preparadas, se deben evitar concatenaciones de cadenas en las consultas SQL. Por ejemplo, evitar prácticas como:

<?php
$emailUsuario = $_POST['email'];
$sql = "SELECT * FROM usuarios WHERE email = '$emailUsuario'";
$resultado = $conexion->query($sql);

Este enfoque es vulnerable a inyección SQL y no debe usarse. En su lugar, siempre se deben utilizar los métodos de enlace de parámetros que ofrecen PDO o MySQLi.

Otro beneficio de las sentencias preparadas es la gestión automática del escape de caracteres especiales en los datos, lo que simplifica el código y reduce errores. No es necesario usar funciones como mysqli_real_escape_string() o realizar escapes manuales cuando se utilizan parámetros enlazados.

Además, las sentencias preparadas manejan correctamente los diferentes tipos de datos, evitando problemas relacionados con comillas, caracteres especiales o formatos específicos.

Para aplicaciones que requieren consultas dinámicas con múltiples parámetros, las sentencias preparadas siguen siendo la opción recomendada. Podemos enlazar varios parámetros de la siguiente manera:

<?php
$sql = 'SELECT * FROM productos WHERE categoria = :categoria AND precio < :precio';
$stmt = $conexion->prepare($sql);
$categoria = $_GET['categoria'];
$precioMaximo = $_GET['precio'];

$stmt->bindParam(':categoria', $categoria, PDO::PARAM_STR);
$stmt->bindParam(':precio', $precioMaximo, PDO::PARAM_INT);
$stmt->execute();

$resultados = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($resultados as $producto) {
    echo 'Producto: ' . $producto['nombre'] . ' - Precio: ' . $producto['precio'] . "\n";
}

En este caso, se enlazan dos parámetros, uno de tipo cadena y otro de tipo entero, garantizando que los datos proporcionados se manejen de forma segura y eficaz.

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

Escapado de valores

El escapado de valores es una técnica esencial para proteger las aplicaciones PHP de la inyección SQL cuando, por alguna razón específica, no es posible utilizar sentencias preparadas. Si bien las sentencias preparadas son la opción recomendada, existen casos donde es necesario aplicar un correcto escapado de los datos proporcionados por el usuario antes de incorporarlos a las consultas SQL.

Una situación común es al interactuar con bases de datos que no soportan sentencias preparadas o cuando se trabaja con funciones que requieren una consulta SQL completa como cadena. En estos casos, es crucial utilizar las funciones de escapado proporcionadas por PHP para asegurar que los datos del usuario no alteren la estructura de la consulta.

Para conexiones con MySQLi, se puede utilizar la función $conexion->real_escape_string(). Esta función escapa caracteres especiales en una cadena para su uso en una instrucción SQL, tomando en cuenta el conjunto de caracteres de la conexión.

<?php
$conexion = new mysqli('localhost', 'mi_usuario', 'mi_contraseña', 'mi_base_de_datos');

if ($conexion->connect_error) {
    die("Error de conexión: $conexion->connect_error\n");
}

$nombreUsuario = $_POST['nombre'];
$nombreEscapado = $conexion->real_escape_string($nombreUsuario);

$sql = "SELECT * FROM usuarios WHERE nombre = '$nombreEscapado'";
$resultado = $conexion->query($sql);

while ($fila = $resultado->fetch_assoc()) {
    echo 'Usuario: ' . $fila['nombre'] . "\n";
}

$conexion->close();

En este ejemplo, el nombre del usuario obtenido de $_POST se escapa antes de incluirse en la consulta SQL. El uso de $conexion->real_escape_string() asegura que caracteres especiales como comillas simples o barras invertidas sean tratados adecuadamente, evitando inyecciones SQL.

Para conexiones utilizando PDO con MySQL, aunque no es habitual escapar manualmente, es posible utilizar la función addslashes() en casos excepcionales:

<?php
$nombreUsuario = $_POST['nombre'];
$nombreEscapado = addslashes($nombreUsuario);

$sql = "SELECT * FROM usuarios WHERE nombre = '$nombreEscapado'";
$stmt = $conexion->query($sql);

while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) {
    echo 'Usuario: ' . $fila['nombre'] . "\n";
}

Sin embargo, es importante tener en cuenta que addslashes() no es tan seguro ni completo como las funciones específicas de cada extensión de base de datos. Además, su uso puede variar dependiendo del motor de base de datos, por lo que es preferible utilizar las funciones de escapado específicas.

Cuando se trabaja con otras bases de datos, como PostgreSQL, se debe utilizar la función adecuada, por ejemplo, pg_escape_string():

<?php
$conexion = pg_connect("host=localhost dbname=mi_base_de_datos user=mi_usuario password=mi_contraseña");

$nombreUsuario = $_POST['nombre'];
$nombreEscapado = pg_escape_string($conexion, $nombreUsuario);

$sql = "SELECT * FROM usuarios WHERE nombre = '$nombreEscapado'";
$resultado = pg_query($conexion, $sql);

while ($fila = pg_fetch_assoc($resultado)) {
    echo 'Usuario: ' . $fila['nombre'] . "\n";
}

pg_close($conexion);

El uso correcto de las funciones de escapado específicas del motor de base de datos es esencial para garantizar la seguridad de la aplicación. Estas funciones tienen en cuenta las particularidades de cada sistema y aplican el escapado adecuado para prevenir inyecciones SQL.

Es fundamental recordar que el escapado de valores debe aplicarse únicamente cuando corresponda y no de manera indiscriminada. Escapar valores que ya han sido escapados puede llevar a problemas como el almacenamiento de datos con caracteres innecesarios o la corrupción de la información.

Además, el escapado de valores no debe sustituir a otras medidas de seguridad. Es necesario combinar el escapado adecuado con otras prácticas como la validación y sanitización de los datos de entrada. Por ejemplo, si se espera un número entero, se debe verificar que el valor proporcionado por el usuario cumpla con ese formato antes de incluirlo en la consulta.

<?php
$idProducto = $_GET['id'];

if (filter_var($idProducto, FILTER_VALIDATE_INT) !== false) {
    $sql = "SELECT * FROM productos WHERE id = $idProducto";
    $resultado = $conexion->query($sql);

    while ($producto = $resultado->fetch_assoc()) {
        echo 'Producto: ' . $producto['nombre'] . "\n";
    }
} else {
    echo "ID de producto inválido.\n";
}

En este ejemplo, se utiliza filter_var() para validar que el ID del producto es un entero válido. Al trabajar con valores numéricos, es preferible validar y utilizar los datos directamente sin colocarlos entre comillas en la consulta SQL.

Es importante también manejar correctamente los conjuntos de caracteres y las configuraciones de conexión para garantizar que el escapado sea efectivo. La conexión a la base de datos debe establecerse con el conjunto de caracteres adecuado, por ejemplo, UTF-8:

<?php
$conexion = new mysqli('localhost', 'mi_usuario', 'mi_contraseña', 'mi_base_de_datos');
$conexion->set_charset('utf8mb4');

Configurar el charset de la conexión asegura que funciones como real_escape_string() manejen correctamente los caracteres especiales y los emojis, evitando posibles vulnerabilidades.

Finalmente, es crucial mantenerse actualizado sobre las mejores prácticas de seguridad y adaptar el código de acuerdo con las recomendaciones actuales. Aunque el escapado de valores puede ser útil en ciertos contextos, siempre que sea posible, se deben preferir las sentencias preparadas y los parámetros enlazados, ya que ofrecen una protección más robusta y reducen el margen de error.

En resumen, el escapado de valores es una herramienta valiosa para proteger nuestras aplicaciones cuando las circunstancias impiden el uso de sentencias preparadas. No obstante, debemos aplicarlo con cuidado, utilizando las funciones adecuadas y complementándolo con una correcta validación y sanitización de los datos de entrada para garantizar la seguridad de nuestra aplicación.

Revisión y filtrado de variables en consultas dinámicas

La revisión cuidadosa y el filtrado de variables en consultas dinámicas es esencial para mantener la seguridad en aplicaciones PHP que interactúan con bases de datos. Cuando se construyen consultas SQL dinámicamente, es fundamental asegurarse de que las variables incorporadas sean seguras y no introduzcan vulnerabilidades de inyección SQL.

Aunque el uso de sentencias preparadas es la práctica recomendada, a veces es necesario crear partes de la consulta de forma dinámica, como al construir cláusulas WHERE basadas en múltiples criterios opcionales. En estos casos, es indispensable validar y filtrar las variables antes de incluirlas en la consulta.

Por ejemplo, supongamos que necesitamos generar una consulta que filtre productos por categoría y rango de precio, donde ambos criterios son opcionales:

<?php
$conexion = new PDO('mysql:host=localhost;dbname=tienda', 'usuario', 'contraseña');
$conexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$sql = 'SELECT * FROM productos';
$whereClauses = [];
$parametros = [];

if (!empty($_GET['categoria'])) {
    $whereClauses[] = 'categoria = :categoria';
    $parametros[':categoria'] = $_GET['categoria'];
}

if (!empty($_GET['precio_min'])) {
    $whereClauses[] = 'precio >= :precio_min';
    $parametros[':precio_min'] = $_GET['precio_min'];
}

if (!empty($_GET['precio_max'])) {
    $whereClauses[] = 'precio <= :precio_max';
    $parametros[':precio_max'] = $_GET['precio_max'];
}

if ($whereClauses) {
    $sql .= ' WHERE ' . implode(' AND ', $whereClauses);
}

$stmt = $conexion->prepare($sql);
$stmt->execute($parametros);

$resultados = $stmt->fetchAll(PDO::FETCH_ASSOC);

foreach ($resultados as $producto) {
    echo 'Producto: ' . $producto['nombre'] . ' - Precio: ' . $producto['precio'] . "\n";
}

En este ejemplo, se construye dinámicamente la cláusula WHERE en función de los parámetros proporcionados por el usuario. Es importante destacar cómo se manejan las variables:

  • Las entradas del usuario ($_GET['categoria'], $_GET['precio_min'], $_GET['precio_max']) se verifican con !empty() para asegurar que existen.
  • Se utilizan placeholders nombrados (:categoria, :precio_min, :precio_max) en la consulta.
  • Los valores de los parámetros se almacenan en un array $parametros, que luego se pasa al método execute().

Este enfoque garantiza que, incluso al construir consultas dinámicas, se sigue utilizando sentencias preparadas y los parámetros son enlazados de forma segura.

Además de este método, es esencial validar y sanear las entradas del usuario. Por ejemplo, si sabemos que el precio debe ser un número, debemos asegurarnos de que los valores proporcionados sean numéricos:

<?php
if (!empty($_GET['precio_min']) && is_numeric($_GET['precio_min'])) {
    $whereClauses[] = 'precio >= :precio_min';
    $parametros[':precio_min'] = $_GET['precio_min'];
}

Al utilizar is_numeric(), verificamos que el valor de precio_min es un número válido antes de incluirlo en la consulta. Esto añade una capa adicional de protección al filtrar las variables según su tipo esperado.

Para valores que deben pertenecer a un conjunto específico, como categorías predefinidas, es recomendable comparar la entrada con una lista permitida:

<?php
$categoriasPermitidas = ['electronica', 'ropa', 'hogar'];

if (!empty($_GET['categoria']) && in_array($_GET['categoria'], $categoriasPermitidas)) {
    $whereClauses[] = 'categoria = :categoria';
    $parametros[':categoria'] = $_GET['categoria'];
}

De esta manera, garantizamos que el valor de categoria es uno de los permitidos y evitamos que se introduzcan valores inesperados o maliciosos.

Otra práctica recomendable es utilizar expresiones regulares para validar formatos específicos. Por ejemplo, si requerimos que un identificador sea un UUID:

<?php
if (!empty($_GET['id']) && preg_match('/^[a-f0-9\-]{36}$/i', $_GET['id'])) {
    $whereClauses[] = 'id = :id';
    $parametros[':id'] = $_GET['id'];
}

La función preg_match() verifica que el valor de id cumple con el patrón esperado antes de ser utilizado en la consulta.

Es importante evitar concatenar directamente las variables del usuario en la consulta, ya que esto abre la puerta a inyecciones SQL. Por ejemplo, nunca debemos hacer lo siguiente:

// ¡NO HACER ESTO!
$sql = "SELECT * FROM productos WHERE categoria = '{$_GET['categoria']}'";

Esta práctica es insegura y debe ser evitada. Siempre es preferible utilizar sentencias preparadas y enlazar los parámetros adecuadamente.

En casos donde sea necesario construir dinámicamente partes de la consulta que no pueden parametrizarse, como nombres de columnas u órdenes de clasificación, debemos sanitizar esas variables con precaución. Por ejemplo:

<?php
$ordenesPermitidos = ['nombre', 'precio', 'fecha'];

$orden = 'nombre'; // Valor por defecto

if (!empty($_GET['orden']) && in_array($_GET['orden'], $ordenesPermitidos)) {
    $orden = $_GET['orden'];
}

$sql .= " ORDER BY $orden";

Aquí, el valor de $orden se restringe a una lista de opciones permitidas para evitar inyecciones SQL en la cláusula ORDER BY.

Del mismo modo, si necesitamos especificar la dirección de orden (ASC o DESC):

<?php
$direccion = 'ASC'; // Valor por defecto

if (!empty($_GET['dir']) && ($_GET['dir'] === 'ASC' || $_GET['dir'] === 'DESC')) {
    $direccion = $_GET['dir'];
}

$sql .= " $direccion";

Al validar estrictamente los valores permitidos, nos aseguramos de que solo se utilicen valores seguros en la consulta.

En resumen, la revisión y filtrado rigurosos de las variables en consultas dinámicas es crucial para la seguridad de nuestras aplicaciones. Debemos:

  • Validar que las entradas del usuario cumplan con los tipos y formatos esperados.
  • Utilizar listas blancas (whitelists) para valores permitidos.
  • Evitar la concatenación directa de variables en la consulta SQL.
  • Utilizar sentencias preparadas y parámetros enlazados siempre que sea posible.

Al aplicar estas prácticas, reforzamos la seguridad de nuestras aplicaciones y protegemos los datos frente a posibles ataques de inyección SQL.

Aprendizajes de esta lección

  • Comprender la importancia de las sentencias preparadas.
  • Implementar sentencias preparadas con PDO y MySQLi.
  • Uso de $stmt->bindParam y $stmt->execute.
  • Diferenciar entre sentencias preparadas y escapado de valores.
  • Aprender a proteger aplicaciones de inyecciones SQL.

Completa PHP y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración