Buenas prácticas y seguridad básica en PHP

Avanzado
PHP
PHP
Actualizado: 29/08/2025

Filtrado de entradas de usuario

La validación y filtrado de datos provenientes del usuario representa una de las primeras líneas de defensa contra vulnerabilidades de seguridad en aplicaciones web. En PHP, nunca debemos confiar en los datos que recibimos del exterior, ya sea a través de formularios, parámetros URL o cualquier otra fuente de entrada.

PHP proporciona herramientas nativas específicamente diseñadas para filtrar y validar datos de entrada de manera segura. La función filter_var() y su familia de funciones relacionadas constituyen el mecanismo estándar para esta tarea.

Tipos de filtrado básico

El filtrado se divide en dos categorías principales: validación (verificar si los datos cumplen criterios específicos) y saneamiento (limpiar los datos eliminando caracteres no deseados).

Para validar un email, utilizamos el filtro específico:

<?php
$email = $_POST['email'] ?? '';

if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "Email válido: " . $email;
} else {
    echo "Email no válido";
}
?>

Para URLs, el proceso es similar:

<?php
$url = $_POST['website'] ?? '';

if (filter_var($url, FILTER_VALIDATE_URL)) {
    echo "URL válida: " . $url;
} else {
    echo "URL no válida";
}
?>

Filtros de saneamiento

Los filtros de saneamiento limpian los datos eliminando o codificando caracteres potencialmente peligrosos. Para strings generales, utilizamos:

<?php
$nombre = $_POST['nombre'] ?? '';

// Elimina etiquetas HTML y caracteres especiales
$nombre_limpio = filter_var($nombre, FILTER_SANITIZE_STRING);

echo "Nombre procesado: " . $nombre_limpio;
?>

Para datos que se mostrarán en HTML, es fundamental prevenir la inyección de código:

<?php
$comentario = $_POST['comentario'] ?? '';

// Codifica caracteres especiales HTML
$comentario_seguro = htmlspecialchars($comentario, ENT_QUOTES, 'UTF-8');

echo "<p>Comentario: " . $comentario_seguro . "</p>";
?>

Validación de números enteros

Para números enteros, PHP ofrece opciones de validación con rangos específicos:

<?php
$edad = $_POST['edad'] ?? '';

$opciones = [
    'options' => [
        'min_range' => 1,
        'max_range' => 120
    ]
];

if (filter_var($edad, FILTER_VALIDATE_INT, $opciones)) {
    echo "Edad válida: " . $edad;
} else {
    echo "Edad debe ser un número entre 1 y 120";
}
?>

Filtrado múltiple con arrays

Cuando manejamos múltiples campos simultáneamente, podemos utilizar filter_var_array():

<?php
$datos = [
    'nombre' => $_POST['nombre'] ?? '',
    'email' => $_POST['email'] ?? '',
    'edad' => $_POST['edad'] ?? ''
];

$filtros = [
    'nombre' => FILTER_SANITIZE_STRING,
    'email' => FILTER_VALIDATE_EMAIL,
    'edad' => [
        'filter' => FILTER_VALIDATE_INT,
        'options' => ['min_range' => 1, 'max_range' => 120]
    ]
];

$datos_filtrados = filter_var_array($datos, $filtros);

// Verificar resultados
foreach ($datos_filtrados as $campo => $valor) {
    if ($valor === false) {
        echo "Error en campo: " . $campo . "\n";
    }
}
?>

Implementación práctica con formularios

Un enfoque robusto para procesar formularios incluye validación previa y mensajes de error claros:

<?php
$errores = [];
$datos_validos = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Validar nombre
    $nombre = trim($_POST['nombre'] ?? '');
    if (empty($nombre)) {
        $errores['nombre'] = 'El nombre es obligatorio';
    } else {
        $datos_validos['nombre'] = filter_var($nombre, FILTER_SANITIZE_STRING);
    }
    
    // Validar email
    $email = $_POST['email'] ?? '';
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errores['email'] = 'Email no válido';
    } else {
        $datos_validos['email'] = $email;
    }
    
    // Si no hay errores, procesar datos
    if (empty($errores)) {
        // Procesar formulario con datos seguros
        echo "Datos procesados correctamente";
    }
}
?>

Validación personalizada

Para casos específicos donde los filtros nativos no son suficientes, podemos crear funciones de validación personalizadas:

<?php
function validarTelefono($telefono) {
    // Eliminar espacios y guiones
    $telefono_limpio = preg_replace('/[\s-]/', '', $telefono);
    
    // Verificar que solo contenga números y tenga 9 dígitos
    if (preg_match('/^\d{9}$/', $telefono_limpio)) {
        return $telefono_limpio;
    }
    
    return false;
}

$telefono = $_POST['telefono'] ?? '';
$telefono_valido = validarTelefono($telefono);

if ($telefono_valido) {
    echo "Teléfono válido: " . $telefono_valido;
} else {
    echo "Formato de teléfono incorrecto";
}
?>

La implementación consistente de estas técnicas de filtrado constituye la base para desarrollar aplicaciones PHP seguras. Cada dato que provenga del exterior debe pasar por este proceso de validación antes de ser utilizado en nuestra aplicación.

Prevención básica de inyección SQL y XSS

Las vulnerabilidades de inyección representan algunas de las amenazas más críticas en aplicaciones web. La inyección SQL permite a atacantes manipular consultas de base de datos, mientras que el Cross-Site Scripting (XSS) posibilita la ejecución de scripts maliciosos en navegadores de usuarios legítimos.

Inyección SQL y consultas preparadas

La inyección SQL ocurre cuando datos no validados se concatenan directamente en consultas SQL. Un atacante puede insertar código SQL malicioso que se ejecutará en la base de datos.

Ejemplo vulnerable (nunca usar):

<?php
// CÓDIGO VULNERABLE - NO USAR
$usuario = $_POST['usuario'];
$password = $_POST['password'];

$query = "SELECT * FROM usuarios WHERE usuario = '$usuario' AND password = '$password'";
$result = mysqli_query($conexion, $query);
?>

La solución segura utiliza consultas preparadas que separan completamente el código SQL de los datos:

<?php
// Usando PDO (recomendado)
$pdo = new PDO('mysql:host=localhost;dbname=miapp', $username, $password);

$usuario = $_POST['usuario'] ?? '';
$password = $_POST['password'] ?? '';

$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE usuario = ? AND password = ?');
$stmt->execute([$usuario, $password]);
$resultado = $stmt->fetch();

if ($resultado) {
    echo "Usuario autenticado";
} else {
    echo "Credenciales incorrectas";
}
?>

Consultas preparadas con parámetros nombrados

Para consultas más complejas, los parámetros nombrados ofrecen mayor claridad:

<?php
$stmt = $pdo->prepare('
    INSERT INTO productos (nombre, precio, categoria_id) 
    VALUES (:nombre, :precio, :categoria)
');

$stmt->execute([
    ':nombre' => $_POST['nombre'],
    ':precio' => $_POST['precio'],
    ':categoria' => $_POST['categoria_id']
]);

echo "Producto insertado correctamente";
?>

Prevención de XSS mediante codificación de salida

El Cross-Site Scripting permite inyectar scripts maliciosos que se ejecutan en el navegador de otros usuarios. La prevención principal consiste en codificar apropiadamente toda salida que provenga de datos de usuario.

Ejemplo vulnerable:

<?php
// VULNERABLE - NO USAR
echo "<h1>Bienvenido " . $_GET['nombre'] . "</h1>";
?>

Solución segura con codificación HTML:

<?php
$nombre = $_GET['nombre'] ?? 'Invitado';
echo "<h1>Bienvenido " . htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8') . "</h1>";
?>

Diferentes contextos de salida XSS

La codificación apropiada depende del contexto donde se muestre la información:

Para contenido HTML:

<?php
$comentario = $_POST['comentario'] ?? '';
echo "<p>" . htmlspecialchars($comentario, ENT_QUOTES, 'UTF-8') . "</p>";
?>

Para atributos HTML:

<?php
$titulo = $_POST['titulo'] ?? '';
echo '<input type="text" value="' . htmlspecialchars($titulo, ENT_QUOTES, 'UTF-8') . '">';
?>

Para JavaScript (requiere codificación específica):

<?php
$mensaje = $_POST['mensaje'] ?? '';
$mensaje_js = json_encode($mensaje, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);

echo "<script>alert($mensaje_js);</script>";
?>

Implementación de Content Security Policy

Una capa adicional de protección contra XSS es implementar Content Security Policy mediante cabeceras HTTP:

<?php
// Establecer CSP restrictiva
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';");

// Resto del contenido de la página
?>

Validación específica para consultas dinámicas

Cuando necesitamos construir consultas con elementos dinámicos como nombres de tabla o columnas (que no pueden parametrizarse), debemos validar contra una lista blanca:

<?php
$columnas_permitidas = ['nombre', 'email', 'fecha_registro'];
$orden_permitido = ['ASC', 'DESC'];

$columna = $_GET['orderby'] ?? 'nombre';
$direccion = strtoupper($_GET['order'] ?? 'ASC');

// Validar contra lista blanca
if (!in_array($columna, $columnas_permitidas) || !in_array($direccion, $orden_permitido)) {
    die('Parámetros de ordenación no válidos');
}

// Ahora es seguro usar en la consulta
$stmt = $pdo->prepare("SELECT * FROM usuarios ORDER BY $columna $direccion");
$stmt->execute();
?>

Manejo seguro de contraseñas (hashing)

El almacenamiento seguro de contraseñas constituye uno de los aspectos más críticos de la seguridad en aplicaciones web. Nunca debemos guardar contraseñas en texto plano ni utilizar algoritmos de hash simples como MD5 o SHA1, que son vulnerables a ataques de fuerza bruta y tablas rainbow.

PHP proporciona funciones nativas específicamente diseñadas para el hashing seguro de contraseñas que implementan algoritmos criptográficamente seguros y técnicas de protección avanzadas como el salting automático.

Hashing de contraseñas con password_hash()

La función password_hash() representa el estándar moderno para cifrar contraseñas en PHP. Utiliza automáticamente técnicas de salting y permite configurar la intensidad computacional:

<?php
$password = $_POST['password'] ?? '';

// Hash seguro con bcrypt (algoritmo por defecto)
$hash = password_hash($password, PASSWORD_DEFAULT);

// Guardar en base de datos
$stmt = $pdo->prepare('INSERT INTO usuarios (email, password_hash) VALUES (?, ?)');
$stmt->execute([$_POST['email'], $hash]);

echo "Usuario registrado correctamente";
?>

Verificación de contraseñas

Para autenticar usuarios, utilizamos password_verify() que compara la contraseña proporcionada con el hash almacenado:

<?php
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';

// Obtener hash de la base de datos
$stmt = $pdo->prepare('SELECT password_hash FROM usuarios WHERE email = ?');
$stmt->execute([$email]);
$usuario = $stmt->fetch();

if ($usuario && password_verify($password, $usuario['password_hash'])) {
    echo "Login exitoso";
    // Iniciar sesión
    $_SESSION['usuario_id'] = $usuario['id'];
} else {
    echo "Credenciales incorrectas";
}
?>

Configuración del costo computacional

El parámetro cost determina la intensidad computacional del algoritmo. Un valor más alto aumenta la seguridad pero también el tiempo de procesamiento:

<?php
$password = $_POST['password'] ?? '';

// Configurar opciones para bcrypt
$opciones = [
    'cost' => 12  // Valor entre 10-15 (12 es recomendado actualmente)
];

$hash = password_hash($password, PASSWORD_BCRYPT, $opciones);

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

Algoritmo Argon2 para mayor seguridad

Argon2 representa el algoritmo más moderno y seguro disponible, ganador del concurso Password Hashing Competition:

<?php
$password = $_POST['password'] ?? '';

// Usar Argon2i (más resistente a ataques de timing)
$hash = password_hash($password, PASSWORD_ARGON2I, [
    'memory_cost' => 65536,  // 64 MB de memoria
    'time_cost' => 4,        // 4 iteraciones
    'threads' => 3           // 3 hilos paralelos
]);

// Para Argon2id (híbrido, más seguro)
$hash_id = password_hash($password, PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,
    'time_cost' => 4,
    'threads' => 3
]);
?>

Validación de fortaleza de contraseñas

Antes de realizar el hashing, debemos validar que la contraseña cumple criterios mínimos de seguridad:

<?php
function validarPassword($password) {
    $errores = [];
    
    if (strlen($password) < 8) {
        $errores[] = 'Debe tener al menos 8 caracteres';
    }
    
    if (!preg_match('/[A-Z]/', $password)) {
        $errores[] = 'Debe contener al menos una mayúscula';
    }
    
    if (!preg_match('/[a-z]/', $password)) {
        $errores[] = 'Debe contener al menos una minúscula';
    }
    
    if (!preg_match('/\d/', $password)) {
        $errores[] = 'Debe contener al menos un número';
    }
    
    if (!preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) {
        $errores[] = 'Debe contener al menos un carácter especial';
    }
    
    return $errores;
}

$password = $_POST['password'] ?? '';
$errores = validarPassword($password);

if (empty($errores)) {
    $hash = password_hash($password, PASSWORD_DEFAULT);
    // Procesar registro
} else {
    foreach ($errores as $error) {
        echo "Error: " . $error . "<br>";
    }
}
?>

Actualización automática de hashes

La función password_needs_rehash() permite actualizar automáticamente hashes cuando cambian los algoritmos o parámetros:

<?php
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';

$stmt = $pdo->prepare('SELECT id, password_hash FROM usuarios WHERE email = ?');
$stmt->execute([$email]);
$usuario = $stmt->fetch();

if ($usuario && password_verify($password, $usuario['password_hash'])) {
    // Login exitoso
    
    // Verificar si necesita rehash (algoritmo más moderno)
    if (password_needs_rehash($usuario['password_hash'], PASSWORD_DEFAULT)) {
        $nuevo_hash = password_hash($password, PASSWORD_DEFAULT);
        
        // Actualizar hash en la base de datos
        $update = $pdo->prepare('UPDATE usuarios SET password_hash = ? WHERE id = ?');
        $update->execute([$nuevo_hash, $usuario['id']]);
    }
    
    $_SESSION['usuario_id'] = $usuario['id'];
}
?>

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en PHP

Documentación oficial de PHP
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, PHP es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de PHP

Explora más contenido relacionado con PHP y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender la importancia del filtrado y validación de datos de usuario en PHP.
  • Aprender a prevenir inyecciones SQL y ataques XSS mediante consultas preparadas y codificación de salida.
  • Implementar hashing seguro de contraseñas usando funciones nativas de PHP.
  • Conocer técnicas para validar la fortaleza de contraseñas y actualizar hashes automáticamente.
  • Aplicar medidas complementarias para mejorar la seguridad en aplicaciones PHP.