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ícateUso 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étodoexecute()
.
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.
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
Introducción Y Entorno
Instalación Y Primer Programa De Php
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Estructuras De Control
Sintaxis
Funciones Y Llamada De Funciones
Sintaxis
Cadenas De Texto Y Manipulación
Sintaxis
Manejo De Números
Sintaxis
Manejo De Fechas Y Tiempo
Sintaxis
Manejo De Arrays
Sintaxis
Introducción A La Poo En Php
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Constructores Y Destructores
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Traits
Programación Orientada A Objetos
Namespaces
Programación Orientada A Objetos
Autoloading De Clases
Programación Orientada A Objetos
Manejo De Errores Y Excepciones
Programación Orientada A Objetos
Manejo De Archivos
Programación Orientada A Objetos
Patrones De Diseño
Programación Orientada A Objetos
Introducción A Los Formularios En Php
Formularios
Procesamiento De Datos De Formularios
Formularios
Manejo De Archivos En Formularios
Formularios
Redirecciones Y Retroalimentación Al Usuario
Formularios
Formularios Dinámicos Y Separación De Lógica
Formularios
Introducción A La Persistencia En Php
Persistencia
Conexión A Bases De Datos
Persistencia
Consultas Y Operaciones Crud
Persistencia
Gestión De Transacciones
Persistencia
Manejo De Errores Y Excepciones En Base De Datos
Persistencia
Patrones De Acceso A Datos
Persistencia
Concepto De Sesiones En Php
Sesiones Y Cookies
Configuración De Sesiones
Sesiones Y Cookies
Cookies
Sesiones Y Cookies
Manejo Avanzado De Sesiones Y Cookies
Sesiones Y Cookies
Principales Vulnerabilidades En Php
Seguridad
Seguridad En Formularios Y Entrada De Datos
Seguridad
Protección Frente A Inyección Sql
Seguridad
Gestión De Contraseñas Y Cifrado
Seguridad
Seguridad En Sesiones Y Cookies
Seguridad
Configuraciones De Php Para Seguridad
Seguridad
Introducción Al Testing En Php
Testing
Phpunit
Testing
Cobertura De Código En Testing
Testing
Test Doubles (Mocks, Stubs, Fakes, Spies)
Testing
Pruebas De Integración Y Funcionales
Testing
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.