Apuntes DAM
Tema 3 de 7DWES · 2º DAW

Desarrollo Web en Entorno Servidor

Formularios, Sesiones y Seguridad

GET y POST, validar, sanear y escapar, XSS, patrón POST/redirección/GET, subida de ficheros, cookies y sesiones, contraseñas con password_hash, control de acceso y tokens CSRF.

70 min lecturaIntermedio

1. Recibir los datos de un formulario

Cuando un usuario envía un formulario, el navegador empaqueta los campos (con su atributo name) y los manda al servidor. PHP los deja preparados en unos arrays especiales llamados superglobales, accesibles desde cualquier parte del script: $_GET para los datos que viajan en la URL, $_POST para los del cuerpo de la petición, $_FILES para los ficheros, $_COOKIE para las cookies y $_SERVER para la información de la petición (método, ruta, IP…).

GETPOST
Dónde van los datosEn la URL: /buscar.php?q=tecladoEn el cuerpo de la petición
Se ven, se guardan en el historial y se pueden compartirSíNo
Úsalo paraBuscar, filtrar, paginar: leer sin cambiar nadaCrear, modificar, borrar, iniciar sesión
Límite de tamañoUnos pocos miles de caracteresGrande (configurable)

La regla práctica: si la petición cambia algo en el servidor, usa POST. Si solo consulta, GET, porque así el usuario puede guardar la búsqueda en marcadores o compartir el enlace.

2. Nunca te fíes de lo que llega

Todo lo que viene del cliente puede estar manipulado: el HTML puede tener un maxlength y un required, pero cualquiera puede enviar la petición a mano con los datos que quiera. Por eso el servidor valida siempre, aunque el formulario ya valide con JavaScript. Hay dos operaciones distintas que conviene no confundir:

Validar, sanear y escapar

Validar
Comprobar que el dato cumple las reglas (un email con formato de email, una edad entre 18 y 120). Si no, se rechaza y se avisa.
Sanear
Limpiar el dato para quedarse con lo útil: quitar espacios, convertir a entero.
Escapar
Al mostrar un dato, convertir sus caracteres especiales para que no se interpreten como código. Se hace en la salida, no en la entrada.
registro.php
1<?php
2declare(strict_types=1);
3
4$errores = [];
5$datos = ['nombre' => '', 'email' => '', 'edad' => ''];
6
7if ($_SERVER['REQUEST_METHOD'] === 'POST') {
8    $datos['nombre'] = trim($_POST['nombre'] ?? '');
9    $datos['email'] = trim($_POST['email'] ?? '');
10    $datos['edad'] = trim($_POST['edad'] ?? '');
11
12    if ($datos['nombre'] === '') {
13        $errores['nombre'] = 'El nombre es obligatorio';
14    }
15    if (!filter_var($datos['email'], FILTER_VALIDATE_EMAIL)) {
16        $errores['email'] = 'El email no es válido';
17    }
18    if (filter_var($datos['edad'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 18]]) === false) {
19        $errores['edad'] = 'Debes ser mayor de edad';
20    }
21
22    if (!$errores) {
23        // … guardar en la base de datos …
24        header('Location: gracias.php');   // POST/redirección/GET
25        exit;
26    }
27}
28?>
29<form method="post" novalidate>
30  <input name="nombre" value="<?= htmlspecialchars($datos['nombre']) ?>">
31  <?php if (isset($errores['nombre'])): ?><span class="error"><?= $errores['nombre'] ?></span><?php endif ?>
32  <!-- … resto de campos … -->
33  <button>Registrarme</button>
34</form>

El ejemplo muestra tres buenas costumbres. Los errores se guardan por campo para mostrarlos junto a cada uno. Si hay errores, el formulario se vuelve a pintar con los valores que el usuario escribió, para que no tenga que empezar de cero. Y si todo va bien, en lugar de mostrar una página directamente se hace una redirección (el patrón POST/redirección/GET): así, si el usuario recarga, no se vuelve a enviar el formulario ni se crea un registro duplicado.

XSS: el error más frecuente en PHP

Escribir echo $_GET['q'] directamente en la página permite que un atacante prepare un enlace con código JavaScript que se ejecutará en el navegador de quien lo abra. Todo dato variable que se imprime en HTML pasa por htmlspecialchars(). Los motores de plantillas como Twig o Blade lo hacen automáticamente.

3. Subir ficheros

Para subir ficheros el formulario necesita method="post" y enctype="multipart/form-data". PHP guarda cada fichero en una carpeta temporal y describe en $_FILES su nombre original, tamaño, tipo y ruta temporal. Tu código decide si lo acepta y, en ese caso, lo mueve a su sitio con move_uploaded_file. Subir ficheros es una de las puertas de entrada favoritas de los atacantes, así que hay que ser estricto:

  1. Comprueba $_FILES['foto']['error'] === UPLOAD_ERR_OK y un tamaño máximo.
  2. No te fíes del tipo que envía el navegador: averigua el real con finfo_file y acepta solo una lista cerrada (image/jpeg, image/png…).
  3. No uses el nombre original: genera uno propio (bin2hex(random_bytes(16)) . '.jpg').
  4. Guarda los ficheros fuera de la carpeta pública o en una carpeta sin permiso de ejecución de PHP.

4. Cookies y sesiones: recordar al usuario

HTTP no tiene memoria: cada petición llega sin saber nada de las anteriores. Para que la aplicación recuerde que has iniciado sesión o qué hay en tu carrito se usan dos mecanismos que trabajan juntos.

Una cookie es un pequeño dato que el servidor pide guardar al navegador (con la cabecera Set-Cookie) y que el navegador devuelve en cada petición siguiente. Vive en el ordenador del usuario, así que puede leerla y modificarla: nunca guardes en ella nada en lo que haya que confiar. Una sesión, en cambio, guarda los datos en el servidor, y en la cookie solo viaja un identificador largo y aleatorio que permite encontrarlos.

login.php
1<?php
2session_start([
3    'cookie_httponly' => true,     // JavaScript no puede leer la cookie
4    'cookie_secure' => true,       // solo viaja por HTTPS
5    'cookie_samesite' => 'Lax',    // no se envía en peticiones de otras webs (protege de CSRF)
6]);
7
8$usuario = buscarUsuario($_POST['email'] ?? '');
9
10if ($usuario && password_verify($_POST['clave'] ?? '', $usuario['hash'])) {
11    session_regenerate_id(true);   // identificador nuevo tras iniciar sesión
12    $_SESSION['usuario_id'] = $usuario['id'];
13    $_SESSION['rol'] = $usuario['rol'];
14    header('Location: panel.php');
15    exit;
16}
17$error = 'Email o contraseña incorrectos';   // no digas cuál de los dos falla
CookieSesión
Dónde se guardan los datosEn el navegadorEn el servidor
Quién puede modificarlosEl usuarioSolo tu código
DuraciónLa que indiques (días, meses)Hasta cerrar el navegador o caducar
Úsala paraPreferencias no críticas: idioma, temaIdentidad, permisos, carrito
CÓMO TRABAJAN LAS SESIONES Y LAS COOKIES EN PHP — OpenWebinars

5. Autenticación y control de acceso

Autenticar es comprobar quién es el usuario; autorizar es decidir qué puede hacer. Las dos cosas se hacen en el servidor, en cada petición, y cualquier fallo aquí es una brecha de seguridad. Estas son las reglas que no se negocian:

Las contraseñas no se guardan, se cifran con un hash

Nunca guardes una contraseña tal cual ni la cifres de forma reversible. password_hash($clave, PASSWORD_DEFAULT) genera un hash con sal aleatoria usando un algoritmo lento a propósito (bcrypt o Argon2), y password_verify($clave, $hash) comprueba si coinciden. Si te roban la base de datos, descifrar esos hashes es muy costoso. MD5 y SHA1 no sirven para esto: son demasiado rápidos.

Protege cada página y cada acción

proteger.php
1function exigirRol(string $rol): void
2{
3    if (!isset($_SESSION['usuario_id'])) {
4        header('Location: login.php');
5        exit;
6    }
7    if ($_SESSION['rol'] !== $rol) {
8        http_response_code(403);
9        exit('No tienes permiso para ver esta página');
10    }
11}
12
13// Al principio de admin.php
14session_start();
15exigirRol('admin');

Tokens contra CSRF

El ataque CSRF (falsificación de petición en sitios cruzados) consiste en que una web maliciosa haga que tu navegador envíe un formulario a otra web donde tienes la sesión abierta. La defensa clásica es un token aleatorio guardado en la sesión e incluido como campo oculto en cada formulario: si al recibir el POST no coinciden, se rechaza. Las cookies con SameSite=Lax añaden una segunda capa.

php
1$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
2// En el formulario: <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
3
4if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
5    http_response_code(419);
6    exit('El formulario ha caducado. Vuelve a intentarlo.');
7}

6. Retos

En estos retos la entrada estándar hace el papel de lo que llegaría del navegador: el cuerpo de un formulario, los comentarios de los usuarios o los intentos de inicio de sesión. El código se ejecuta con PHP 8.3 real.

🐘PHPAsí llegan los datos de un formulario

parse_str convierte el cuerpo de una petición en un array, igual que PHP hace con $_POST.

Ejecuta el código para ver la salida

Retos de PHP en el IDE

Escribe el código que falta y pulsa Ejecutar tests: se ejecuta con PHP 8.3 real y se prueba con varios casos, algunos ocultos.

🐘PHPValidar los datos de un formularioMedio

La entrada es el cuerpo de un formulario enviado por POST (nombre=Ana&email=…&edad=…&web=…). Valida y escribe un error por línea, en este orden: «nombre: obligatorio» o «nombre: máximo 50 caracteres»; «email: no válido»; «edad: debe estar entre 18 y 120» (entero); «web: no válida» (es opcional, solo se valida si no está vacía). Si no hay errores, escribe «Datos correctos».

⏳
Formulario correcto
⏳
Todo mal
⏳
Nombre largo y edad decimal
⏳
Test oculto #4
0/4 tests pasados
🐘PHPComentarios sin XSSFácil

Cada línea es un comentario «autor|texto» que han escrito los usuarios. El código ya los muestra como <li>, pero es vulnerable: un usuario podría colar HTML o JavaScript. Corrígelo para que los caracteres especiales se escapen (&lt; &gt; &amp; &quot; &#039;).

⏳
Comentarios normales
⏳
Intento de ataque
⏳
Test oculto #3
0/3 tests pasados
🐘PHPInicio de sesión con bloqueoDifícil

Las contraseñas se guardan cifradas con password_hash. Cada línea de la entrada es un intento «usuario contraseña» (la contraseña es lo que va después del primer espacio). Escribe «Bienvenido/a, usuario» si es correcto o «Usuario o contraseña incorrectos» si no (sin revelar si el usuario existe). Tras 3 fallos seguidos de un mismo usuario, su cuenta queda bloqueada: responde «Cuenta bloqueada» a cualquier intento posterior, aunque la contraseña sea correcta. Un acceso correcto reinicia sus fallos.

⏳
Accesos correctos e incorrectos
⏳
Bloqueo tras tres fallos
⏳
Test oculto #3
0/3 tests pasados

¿Has terminado este tema?

Crea una cuenta gratis para guardar qué temas has terminado, subir de nivel y ganar medallas.

Guardar mi progreso