Curso de PHP
Seguridad en aplicaciones PHP: las 8 reglas esenciales
Por Víctor Peña · Publicado el
Hola, ¿cómo están? Tercer tema complementario del curso de PHP, y probablemente el más importante de todos.
A lo largo del curso fueron apareciendo advertencias de seguridad sueltas. Aquí las reunimos y añadimos las que faltaban, porque un sistema que funciona pero es inseguro no está terminado.
¡Empecemos!
1. Inyección SQL
El riesgo: un atacante escribe SQL dentro de un campo y toma el control de la consulta.
<?php
// Vulnerable
$id = $_GET['id'];
$pdo->query("SELECT * FROM usuarios WHERE id = $id");
Con ?id=1 OR 1=1 devuelve todos los usuarios. Con un poco más, borra tablas.
La solución: consultas preparadas, siempre.
<?php
$sentencia = $pdo->prepare("SELECT * FROM usuarios WHERE id = :id");
$sentencia->execute(['id' => $_GET['id']]);
Un detalle: los nombres de tabla y columna no se pueden parametrizar. Si necesitas ordenar por una columna que llega del usuario, valídala contra una lista blanca:
<?php
$permitidas = ['nombre', 'precio', 'fecha'];
$orden = in_array($_GET['orden'] ?? '', $permitidas, true) ? $_GET['orden'] : 'nombre';
2. XSS: código inyectado en tus páginas
El riesgo: guardas lo que escribió un usuario y lo imprimes tal cual. Si escribió un <script>, se ejecuta en el navegador de quien vea la página.
La solución: escapar siempre al mostrar.
<?php
echo htmlspecialchars($comentario, ENT_QUOTES, 'UTF-8');
Como vimos en formularios: escapa al mostrar, no al guardar.
Si necesitas permitir algo de HTML —un editor de texto enriquecido—, no lo filtres a mano: usa una librería especializada como HTML Purifier. Escribir tu propio filtro de HTML es garantía de dejar un agujero.
3. CSRF: peticiones desde otro sitio
El riesgo: este es menos conocido y muy real. Un usuario con sesión abierta en tu sistema visita otra web, y esa web envía una petición a la tuya. El navegador adjunta la cookie de sesión automáticamente, así que la petición llega autenticada.
La solución: un token único por sesión que acompañe cada formulario.
<?php
// Al generar el formulario
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<!-- ... -->
</form>
<?php
// Al procesarlo
$recibido = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'] ?? '', $recibido)) {
http_response_code(403);
exit('Petición no válida');
}
Usa hash_equals() y no ===: compara en tiempo constante y evita ataques por medición de tiempos.
Todo formulario que modifique datos necesita token CSRF. Los frameworks lo ponen automáticamente; a mano hay que acordarse.
4. Contraseñas
Nunca en texto plano. Nunca con md5() o sha1(). Esos algoritmos son rapidísimos, y esa velocidad es justo lo que permite probar millones de combinaciones por segundo.
<?php
// Al registrar
$hash = password_hash($clave, PASSWORD_DEFAULT);
// Al verificar
if (password_verify($claveIngresada, $hashGuardado)) {
// correcto
}
password_hash() genera una sal aleatoria automáticamente y usa un algoritmo diseñado para ser lento. Si mañana PHP cambia el algoritmo por defecto, PASSWORD_DEFAULT se actualiza solo.
Y en el login, mensajes genéricos: «correo o contraseña incorrectos», nunca «ese correo no existe».
5. Subida de archivos
Es el punto más peligroso de cualquier aplicación, porque un archivo subido puede ser código ejecutable.
<?php
$archivo = $_FILES['imagen'];
// 1. Comprobar que no hubo error
if ($archivo['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Error al subir el archivo');
}
// 2. Validar el tipo REAL, no la extensión ni el tipo declarado
$finfo = new finfo(FILEINFO_MIME_TYPE);
$tipo = $finfo->file($archivo['tmp_name']);
$permitidos = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
if (!isset($permitidos[$tipo])) {
throw new RuntimeException('Tipo de archivo no permitido');
}
// 3. Validar el tamaño
if ($archivo['size'] > 2 * 1024 * 1024) {
throw new RuntimeException('El archivo supera los 2 MB');
}
// 4. Generar un nombre nuevo: nunca usar el original
$nombre = bin2hex(random_bytes(16)) . '.' . $permitidos[$tipo];
// 5. Guardar fuera de la carpeta pública si es posible
move_uploaded_file($archivo['tmp_name'], __DIR__ . '/../almacen/' . $nombre);
Los cinco pasos importan, pero dos son críticos:
- No confíes en
$_FILES['imagen']['type']. Lo envía el navegador y se puede falsificar. Comprueba el tipo real confinfo. - Nunca uses el nombre original. Un archivo llamado
foto.php.jpgen un servidor mal configurado puede acabar ejecutándose.
6. Credenciales fuera del código
Como advertimos en constantes: las contraseñas de base de datos y las claves de API no van en archivos que llegan al repositorio.
Un archivo de configuración aparte, incluido en el .gitignore, y leído desde ahí. Y recuerda: si una credencial llegó alguna vez a un repositorio, cámbiala. Sigue en el historial aunque borres el archivo.
7. Configuración del servidor
En producción:
<?php
ini_set('display_errors', '0');
ini_set('log_errors', '1');
error_reporting(E_ALL);
Como vimos en gestión de errores, un mensaje de error revela rutas, nombres de tablas y versiones de software.
Además:
- Usa HTTPS. Sin él, las cookies y contraseñas viajan en texto plano.
- Mantén PHP actualizado. Una versión sin soporte es una versión sin parches de seguridad.
- Quita del servidor cualquier
phpinfo.php,test.phpo copia de seguridad que hayas dejado.
8. Valida siempre en el servidor
Todo lo que llega del cliente es sospechoso: formularios, URL, cabeceras y cookies. Los atributos required del HTML son comodidad para el usuario, no seguridad.
Y aplica el principio del mínimo privilegio: el usuario de base de datos de tu aplicación no necesita permisos para borrar tablas. Dale solo lo que usa.
Una lista para revisar antes de publicar
- Todas las consultas usan parámetros
- Toda salida pasa por
htmlspecialchars() - Los formularios que modifican datos llevan token CSRF
- Las contraseñas usan
password_hash() - Las subidas validan tipo real, tamaño y renombran el archivo
- Las credenciales están fuera del repositorio
-
display_errorsestá desactivado - El sitio usa HTTPS
-
session_regenerate_id(true)tras el login - Las cookies usan
httponly,secureysamesite
Para cerrar
La seguridad no es un módulo que se agrega al final: es una forma de escribir cada línea. Y la buena noticia es que en PHP moderno casi todo está resuelto —password_hash, consultas preparadas, htmlspecialchars—; solo hay que usarlo.
Si tienes que quedarte con una sola idea: nunca confíes en un dato que no generaste tú.
En el siguiente artículo veremos el manejo de fechas.
Saludos y éxitos.
Norvic Software
Desarrollamos el software que tu empresa necesita
Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.
- Chatbots con Inteligencia Artificial
- Creación de agentes de IA
- Integración de IA en tus sistemas
- Desarrollo de software a medida
- Aplicaciones móviles iOS y Android
- Consultoría y asesoramiento técnico
Cotización sin costo · Respuesta directa por WhatsApp