PHP

PHP

Tutorial PHP: Protección frente a Inyección SQL

PHP: Aprende a implementar sentencias preparadas para evitar inyecciones SQL y mejorar la seguridad de tus aplicaciones web.

Aprende PHP GRATIS y certifícate

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.

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.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

20 % DE DESCUENTO

Plan mensual

19.00 /mes

15.20 € /mes

Precio normal mensual: 19 €
58 % DE DESCUENTO

Plan anual

10.00 /mes

8.00 € /mes

Ahorras 132 € al año
Precio normal anual: 120 €
Aprende PHP GRATIS online

Todas las lecciones de PHP

Accede a todas las lecciones de PHP y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Php

PHP

Introducción Y Entorno

Instalación Y Primer Programa De Php

PHP

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

PHP

Sintaxis

Operadores Y Expresiones

PHP

Sintaxis

Estructuras De Control

PHP

Sintaxis

Funciones Y Llamada De Funciones

PHP

Sintaxis

Cadenas De Texto Y Manipulación

PHP

Sintaxis

Manejo De Números

PHP

Sintaxis

Manejo De Fechas Y Tiempo

PHP

Sintaxis

Manejo De Arrays

PHP

Sintaxis

Introducción A La Poo En Php

PHP

Programación Orientada A Objetos

Clases Y Objetos

PHP

Programación Orientada A Objetos

Constructores Y Destructores

PHP

Programación Orientada A Objetos

Herencia

PHP

Programación Orientada A Objetos

Encapsulación

PHP

Programación Orientada A Objetos

Polimorfismo

PHP

Programación Orientada A Objetos

Interfaces

PHP

Programación Orientada A Objetos

Traits

PHP

Programación Orientada A Objetos

Namespaces

PHP

Programación Orientada A Objetos

Autoloading De Clases

PHP

Programación Orientada A Objetos

Manejo De Errores Y Excepciones

PHP

Programación Orientada A Objetos

Manejo De Archivos

PHP

Programación Orientada A Objetos

Patrones De Diseño

PHP

Programación Orientada A Objetos

Introducción A Los Formularios En Php

PHP

Formularios

Procesamiento De Datos De Formularios

PHP

Formularios

Manejo De Archivos En Formularios

PHP

Formularios

Redirecciones Y Retroalimentación Al Usuario

PHP

Formularios

Formularios Dinámicos Y Separación De Lógica

PHP

Formularios

Introducción A La Persistencia En Php

PHP

Persistencia

Conexión A Bases De Datos

PHP

Persistencia

Consultas Y Operaciones Crud

PHP

Persistencia

Gestión De Transacciones

PHP

Persistencia

Manejo De Errores Y Excepciones En Base De Datos

PHP

Persistencia

Patrones De Acceso A Datos

PHP

Persistencia

Concepto De Sesiones En Php

PHP

Sesiones Y Cookies

Configuración De Sesiones

PHP

Sesiones Y Cookies

Cookies

PHP

Sesiones Y Cookies

Manejo Avanzado De Sesiones Y Cookies

PHP

Sesiones Y Cookies

Principales Vulnerabilidades En Php

PHP

Seguridad

Seguridad En Formularios Y Entrada De Datos

PHP

Seguridad

Protección Frente A Inyección Sql

PHP

Seguridad

Gestión De Contraseñas Y Cifrado

PHP

Seguridad

Seguridad En Sesiones Y Cookies

PHP

Seguridad

Configuraciones De Php Para Seguridad

PHP

Seguridad

Introducción Al Testing En Php

PHP

Testing

Phpunit

PHP

Testing

Cobertura De Código En Testing

PHP

Testing

Test Doubles (Mocks, Stubs, Fakes, Spies)

PHP

Testing

Pruebas De Integración Y Funcionales

PHP

Testing

Accede GRATIS a PHP y certifícate

En esta lección

Objetivos de aprendizaje 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.