Ir al contenido principal

Lección 3 · Autoevaluación

Cómo se usa

Esta página cubre todo lo de la lección 3: el ida y vuelta del formulario, GET y POST, la lectura y validación de datos, el token CSRF, la redirección después de guardar, cookies, sesiones, el login y los permisos.

Hay 24 preguntas de opción múltiple y 10 ejercicios. En las preguntas la corrección es inmediata: al elegir una opción se marca en verde la correcta y aparece la explicación.

Los ejercicios son de escritura. Los de esta lección tienen una particularidad: casi todos son código que se puede ejecutar en el servidor local, así que conviene probarlos —abrir el formulario, enviarlo mal a propósito, ver qué llega—. Un formulario que nunca se probó con datos sucios no está terminado.

Preguntas

Agrupadas por tema, en el orden en que se dieron en la lección. Una sola opción es correcta en cada una.

Todavía sin contestar · 24 preguntas

El ida y vuelta, GET y POST

1. Desde que alguien abre un formulario hasta que el dato queda guardado, ¿cuántas peticiones hay?

Elegí una opción para verla

Dos peticiones separadas: la que devuelve el formulario y la que envía los datos. Son dos ejecuciones distintas del programa.

Por eso nada de lo que había en las variables de la primera existe en la segunda: los datos viajan dentro de la petición.

2. En un formulario con method="get", ¿dónde quedan los datos?

Elegí una opción para verla

En la dirección, después del ?. Quedan en el historial, en los registros del servidor y en cualquier enlace que se copie.

Por eso GET sirve para consultar y nunca para enviar contraseñas.

3. ¿Cuál de estas operaciones corresponde a POST?

Elegí una opción para verla

El alta de un cliente, porque crea algo. Las otras tres solo leen, y conviene que la dirección se pueda compartir y repetir.

La regla: GET para leer, POST para escribir.

4. ¿Por qué modificar datos con GET es peligroso?

Elegí una opción para verla

Porque basta con que el usuario entre a otra página que tenga una imagen apuntando a http://localhost/tienda/borrar.php?codigo=1042. El navegador hace la petición solo, con la cookie de sesión del usuario.

Es el ataque CSRF, y con GET es trivial.

5. ¿Qué pasa con las variables de la página que muestra el formulario cuando llega la petición que lo procesa?

Elegí una opción para verla

No existen. Cada petición empieza de cero, y es la limitación que hace necesarias las sesiones.

Lo único que sobrevive son los datos que viajan en la propia petición.

El formulario y la lectura de datos

6. Un campo tiene id="precio" pero le falta name. ¿Qué pasa al enviar?

Elegí una opción para verla

Sin name el campo no se envía. El usuario lo completa y el servidor no se entera nunca.

El id es para el for de la etiqueta y para los lectores de pantalla; el name es para PHP.

7. Si un <form> no tiene atributo method, ¿cómo se envía?

Elegí una opción para verla

Como GET, que es el valor por defecto. Es una de las razones por las que un formulario que guarda datos «no hace nada»: los datos van en la dirección y el archivo los busca en $_POST, que está vacío.

8. ¿Qué resuelve $_POST['precio'] ?? ''?

Elegí una opción para verla

Evita el aviso de clave inexistente y deja '' en lugar de null. Así la validación de «obligatorio» puede detectarlo y el programa no sigue con un null que se comporta como cero.

9. ¿Por qué se aplica trim() a lo que llega de un formulario?

Elegí una opción para verla

Un código que en pantalla parece 1042 puede ser " 1042", y entonces " 1042" === "1042" es falso. El error es invisible y cuesta una tarde encontrarlo.

trim() quita espacios de los extremos y no toca los del medio.

10. ¿De qué tipo son los datos que llegan por $_POST?

Elegí una opción para verla

Siempre texto. El type="number" cambia el teclado del celular y la ayuda del navegador, pero no impide que alguien arme la petición a mano.

Por eso se convierte con (int) o (float), y por eso conviene declare(strict_types=1).

Validación y errores

11. ¿Dónde se decide si un dato sirve?

Elegí una opción para verla

En el servidor. La validación del navegador ayuda al usuario, pero se saltea abriendo las herramientas del desarrollador o armando la petición a mano.

Las restricciones de la base son la última red, no el único control: si llega un dato inválido, lo ideal es no haberlo intentado guardar.

12. ¿Qué problema tiene in_array($categoria, CATEGORIAS) sin el tercer argumento?

Elegí una opción para verla

Compara con ==. En una lista blanca de categorías o de roles, esa comparación floja es un agujero.

Se usa in_array($categoria, CATEGORIAS, true).

13. ¿Con qué condición se decide que la validación pasó y se puede guardar?

Elegí una opción para verla

if ($errores === []). Es explícito y no depende de cómo se evalúa un array vacío en una condición.

Todo lo que modifica el sistema va adentro de ese if: es lo que evita guardar un producto con precio inválido porque la validación se hizo después.

14. ¿Cómo conviene guardar los mensajes de error?

Elegí una opción para verla

En un array con la clave del campo ($errores['precio']), para poder mostrarlo al lado del campo que lo produjo.

«Revisá los datos» no le dice a nadie qué corregir.

15. Al volver a mostrar el formulario con lo que el usuario cargó, ¿qué hay que hacer con el valor?

Elegí una opción para verla

Escaparlo. Un nombre con una comilla cierra el atributo y rompe el formulario; con etiquetas adentro, ejecuta código en el navegador de quien lo abre.

value="<?= htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8') ?>"

Token, redirección y cookies

16. ¿Para qué sirve el token CSRF en un formulario?

Elegí una opción para verla

El token es un valor aleatorio guardado en la sesión y escrito en el formulario. La otra página no puede conocerlo, así que no puede armar una petición que pase la comprobación.

Otra página no puede enviar un POST a otro dominio ni leer el token, por la política de mismo origen.

17. ¿Por qué el token se compara con hash_equals() y no con ===?

Elegí una opción para verla

La comparación con === corta en el primer carácter distinto: midiendo cuánto tarda se puede ir adivinando el token carácter por carácter. hash_equals() recorre siempre todo, así que el tiempo no dice nada.

18. Después de guardar un dato con éxito, ¿qué corresponde hacer?

Elegí una opción para verla

Redirigir. Si la confirmación se muestra como respuesta al POST, al recargar el navegador reenvía el formulario y el dato se guarda dos veces.

El patrón se llama POST, redirección, GET.

19. ¿Qué pasa si se llama a header('Location: ...') y se olvida el exit?

Elegí una opción para verla

La redirección se manda, pero PHP sigue ejecutando el resto del archivo. Puede volver a guardar el dato, hacer otra consulta o imprimir contenido que ya no se ve.

Por eso header() y exit van juntos, siempre.

20. ¿Dónde vive una cookie y qué implica eso?

Elegí una opción para verla

En el navegador, y se puede editar. Sirve para preferencias —tema, idioma— y para llevar el identificador de la sesión.

Nunca para guardar el rol, un precio o si alguien pagó: eso vive en el servidor.

Sesiones, login y permisos

21. ¿Dónde se guarda el contenido de $_SESSION?

Elegí una opción para verla

En el servidor. El navegador solo lleva una cookie con el identificador de la sesión; con ese identificador, PHP busca el array correspondiente en cada petición.

Por eso el rol guardado en la sesión no se puede cambiar editando una cookie.

22. ¿Qué hace session_regenerate_id(true) al iniciar sesión y para qué?

Elegí una opción para verla

Cambia el identificador y elimina el archivo de la sesión anterior. Si alguien había conseguido el identificador previo, deja de servirle.

Es el ataque llamado session fixation: el atacante fija un identificador y espera a que la víctima se autentique con él.

23. ¿Cómo se comprueba una contraseña guardada en la base?

Elegí una opción para verla

Con password_verify(), que compara la clave con el hash sin necesidad de conocerla. La contraseña nunca se guarda.

hash_equals también compara en tiempo constante, pero sirve para tokens, no para contraseñas: el hash tiene que verificarlo la función que entiende su formato.

24. ¿Cuál es la forma correcta de cerrar sesión?

Elegí una opción para verla

Las tres cosas. Con solo unset($_SESSION['usuario']) la sesión sigue viva: el archivo del servidor existe, la cookie sigue en el navegador y cualquier otra clave —el rol, el token— sobrevive.

Y si el control de acceso mira el rol, el usuario sigue pasando.

Ejercicios de práctica

Diez ejercicios. Los primeros cinco son de formulario y validación; los últimos agregan token, sesiones, login y permisos. Todos se pueden probar en el servidor local.

Ejercicio 1

Escribir el formulario de alta de un producto con código, nombre, categoría, precio y stock, con los atributos que hacen falta para que los datos lleguen a PHP por POST.

<form action="guardar-producto.php" method="post">
    <label for="codigo">Código</label>
    <input type="text" id="codigo" name="codigo" required>

    <label for="nombre">Nombre</label>
    <input type="text" id="nombre" name="nombre" maxlength="80" required>

    <label for="categoria">Categoría</label>
    <select id="categoria" name="categoria">
        <option value="">Elegí una</option>
        <option value="almacen">Almacén</option>
        <option value="bebidas">Bebidas</option>
        <option value="limpieza">Limpieza</option>
    </select>

    <label for="precio">Precio</label>
    <input type="number" id="precio" name="precio" step="0.01" min="0">

    <label for="stock">Stock</label>
    <input type="number" id="stock" name="stock" min="0">

    <button type="submit">Guardar</button>
</form>

Lo que se evalúa: method="post", el action correcto, y que cada campo tenga name además de id.

Ejercicio 2

Leer esos cinco campos sin que aparezcan avisos si alguno no vino, y dejarlos listos para validar.

declare(strict_types=1);

$codigo    = trim($_POST['codigo'] ?? '');
$nombre    = trim($_POST['nombre'] ?? '');
$categoria = $_POST['categoria'] ?? '';
$precio    = $_POST['precio'] ?? '';
$stock     = $_POST['stock'] ?? '';

Todos quedan como texto: la conversión a número se hace después de validar que el texto era un número. Si se convierte antes, un "abc" se transforma en 0 y la validación ya no puede distinguir un error de un cero legítimo.

Ejercicio 3

Escribir la validación del alta, con los errores guardados por campo.

const CATEGORIAS = ['almacen', 'bebidas', 'limpieza'];

$errores = [];

if ($codigo === '' || !ctype_digit($codigo)) {
    $errores['codigo'] = 'El código tiene que ser un número.';
}

if ($nombre === '') {
    $errores['nombre'] = 'El nombre es obligatorio.';
} elseif (mb_strlen($nombre) > 80) {
    $errores['nombre'] = 'El nombre no puede pasar de 80 caracteres.';
}

if (!in_array($categoria, CATEGORIAS, true)) {
    $errores['categoria'] = 'Elegí una categoría de la lista.';
}

if ($precio === '' || !is_numeric($precio)) {
    $errores['precio'] = 'El precio tiene que ser un número.';
} elseif ((float) $precio < 0) {
    $errores['precio'] = 'El precio no puede ser negativo.';
}

if ($stock === '' || !ctype_digit($stock)) {
    $errores['stock'] = 'El stock tiene que ser un número entero.';
}

Ejercicio 4

Mostrar el campo del nombre con su error debajo y sin perder lo cargado.

<div class="mb-3">
    <label for="nombre" class="form-label">Nombre</label>
    <input type="text" id="nombre" name="nombre"
           class="form-control <?= isset($errores['nombre']) ? 'is-invalid' : '' ?>"
           value="<?= htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8') ?>">

    <?php if (isset($errores['nombre'])): ?>
        <div class="invalid-feedback">
            <?= htmlspecialchars($errores['nombre'], ENT_QUOTES, 'UTF-8') ?>
        </div>
    <?php endif; ?>
</div>

Dos escapes y no uno: el valor y el mensaje. Los dos vienen de datos que escribió el usuario.

Ejercicio 5

Agregar el token CSRF: generarlo, escribirlo en el formulario y comprobarlo al procesar.

<?php
session_start();

if (empty($_SESSION['token'])) {
    $_SESSION['token'] = bin2hex(random_bytes(32));
}
?>
<form action="guardar-producto.php" method="post">
    <input type="hidden" name="token" value="<?= $_SESSION['token'] ?>">
<?php
// guardar-producto.php, después de session_start()
if (!hash_equals($_SESSION['token'] ?? '', $_POST['token'] ?? '')) {
    http_response_code(419);
    exit('La sesión venció. Volvé a cargar el formulario.');
}

Ejercicio 6

Completar el guardado: si no hay errores, redirigir avisando que salió bien. Explicar por qué no se muestra el mensaje en la misma respuesta.

if ($errores === []) {
    // ... insertar el producto ...

    header('Location: productos.php?alta=ok');
    exit;
}

Porque si el mensaje se muestra como respuesta al POST, al recargar el navegador reenvía el formulario y el producto se guarda dos veces. Con la redirección, la recarga hace un GET y no pasa nada.

Ejercicio 7

Escribir el login completo: formulario, comprobación con la clave hasheada y apertura de la sesión.

<?php

declare(strict_types=1);

session_set_cookie_params(['httponly' => true, 'samesite' => 'Lax']);
session_start();

const USUARIOS = [
    'vendedor1' => [
        'hash' => '$2y$12$79ITnHymNoeM0m7EvD/PGu0KnuPRVXMVxjvIySSHmBB5Zw0vfbjFq',
        'rol'  => 'vendedor',
    ],
];

$error = '';
$usuario = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $usuario = trim($_POST['usuario'] ?? '');
    $clave = $_POST['clave'] ?? '';

    if ($usuario === '' || $clave === '') {
        $error = 'Completá usuario y contraseña.';
    } elseif (!isset(USUARIOS[$usuario])
        || !password_verify($clave, USUARIOS[$usuario]['hash'])) {
        $error = 'Usuario o contraseña incorrectos.';
    } else {
        session_regenerate_id(true);
        $_SESSION['usuario'] = $usuario;
        $_SESSION['rol'] = USUARIOS[$usuario]['rol'];

        header('Location: panel.php');
        exit;
    }
}

Ese hash corresponde a la clave esquina2026: el ejemplo se puede ejecutar tal cual.

Ejercicio 8

Proteger catalogo-editar.php para que solo entre el administrador. Indicar dónde va el control.

<?php

declare(strict_types=1);

session_start();

if (!isset($_SESSION['usuario'])) {
    header('Location: login.php');
    exit;
}

if (($_SESSION['rol'] ?? '') !== 'administrador') {
    http_response_code(403);
    exit('No tenés permiso para editar el catálogo.');
}

En la primera línea, antes de cualquier salida, incluido el DOCTYPE. Y el control va en el archivo que procesa, no solo en el que muestra el enlace: ocultar un enlace no protege nada.

Ejercicio 9

Un formulario de búsqueda envía el texto por GET. Escribir la recepción y el filtro del catálogo, escapando al mostrar.

$busqueda = trim($_GET['q'] ?? '');

$encontrados = [];

if ($busqueda !== '') {
    $encontrados = array_filter(
        $catalogo,
        fn (array $p): bool => str_contains(
            normalizar($p['nombre']),
            normalizar($busqueda)
        )
    );
}
?>
<p>Resultados para: <?= htmlspecialchars($busqueda, ENT_QUOTES, 'UTF-8') ?></p>

GET porque es una consulta: la dirección se puede compartir y recargar sin consecuencias. Y el texto de la búsqueda se escapa al mostrarlo, como cualquier dato que venga de afuera.

Ejercicio 10

Explicar qué le falta a este cierre de sesión y escribirlo completo: unset($_SESSION['usuario']); header('Location: login.php');

<?php

declare(strict_types=1);

session_start();

$_SESSION = [];

if (ini_get('session.use_cookies')) {
    $p = session_get_cookie_params();
    setcookie(session_name(), '', [
        'expires'  => time() - 42000,
        'path'     => $p['path'],
        'httponly' => true,
        'secure'   => true,
        'samesite' => 'Lax',
    ]);
}

session_destroy();

header('Location: login.php');
exit;

Le faltan tres cosas: vaciar el array completo (otras claves como el rol o el token sobreviven), borrar la cookie y destruir la sesión en el servidor. Y sin session_start() no hay sesión que cerrar.

Cómo saber si está bien

Si fallaste… Volvé a
Las preguntas 1 a 5El ida y vuelta y GET y POST.
Las preguntas 6 a 10El formulario y Leer los datos.
Las preguntas 11 a 15Validar en el servidor y Mostrar los errores y no perder lo cargado.
Las preguntas 16 a 20El token del formulario, Después de guardar, redirigir y Cookies.
Las preguntas 21 a 24Sesiones, El login del vendedor y Quién puede hacer qué.
Los ejercicios 7, 8 y 10Errores frecuentes: son los del login y la sesión, los que más se rompen.

Con 18 o más respuestas correctas, la lección está firme. Entre 12 y 17, conviene releer los temas que fallaron. Con menos de 12, la lección 4 va a costar: los archivos y las excepciones se apoyan en la validación y en el manejo de errores que se ven acá.

Lo que hay que poder explicar

Idea La respuesta en una línea
Dos peticionesEl formulario y su procesamiento son ejecuciones distintas; nada sobrevive entre una y otra.
GET y POSTLeer por GET, escribir por POST; modificar por GET abre la puerta al CSRF.
El nameEs la clave con la que el dato llega a PHP. Sin él, el campo no existe del otro lado.
ValidarEn el servidor, con errores por campo y una sola condición para seguir.
TokenAleatorio, guardado en la sesión, comparado con hash_equals.
Redirigir después de guardarheader('Location: …') y exit: recargar no duplica.
CookiesEstán en el navegador y se editan: nunca guardar ahí algo que haya que creer.
SesionesViven en el servidor; el navegador solo lleva el identificador.
Contraseñaspassword_hash() al guardar, password_verify() al comprobar.
PermisosSe controlan en cada archivo del servidor, no en el menú.

La lección 4 toma todo esto y lo aplica a lo que el sistema todavía no sabe hacer: leer y escribir archivos, recibir una foto de un producto y registrar lo que falla.

Licencia. El material de esta página es elaboración propia de Prof Mauro Bobyk —a partir de las fuentes que detalla el pie del sitio— y se publica bajo una licencia Creative Commons BY-NC-SA 4.0.