PHPlay

Curso de PHP gratis · Capítulo 12 de 13 · con Rex Request

PHP en la web: $_GET, $_POST, XSS, sesiones y cookies

PHP nació para la web. Verás el viaje de una petición HTTP, cómo leer formularios con $_GET y $_POST y cómo protegerte del XSS con htmlspecialchars.

El viaje de una petición

Cuando escribes una dirección en el navegador, pasa esto:

  1. El navegador envía una petición HTTP a un servidor: "dame /posada.php?plato=sopa".
  2. El servidor web (Apache, Nginx...) ve que es un archivo .php y se lo pasa al intérprete de PHP.
  3. PHP ejecuta el script desde el principio, con los datos de la petición ya preparados en unas variables especiales ($_GET, $_POST, $_SERVER...).
  4. Todo lo que el script imprime con echo (y el HTML de fuera de <?php) forma la respuesta, que vuelve al navegador.
  5. El navegador dibuja ese HTML.
  6. PHP lo olvida todo: variables, arrays, objetos... desaparecen.
Pergamino del porque

A este modelo se le llama shared-nothing (no compartir nada): cada petición vive aislada. ¿Por qué es bueno? Porque un error o un dato basura de una petición nunca contamina a la siguiente, y porque es trivial repartir las peticiones entre muchos servidores.

El programa de la derecha es exactamente lo que PHP le devolvería al navegador.

<!DOCTYPE html>
<html>
<body>
<h1>Posada del Request</h1>
<p>Hoy es <?= "martes" ?> y servimos <?= 3 * 4 ?> raciones.</p>
</body>
</html>
Salida
<!DOCTYPE html>
<html>
<body>
<h1>Posada del Request</h1>
<p>Hoy es martes y servimos 12 raciones.</p>
</body>
</html>

PHP escribe HTML

Para el navegador, la respuesta es solo texto: HTML. PHP no "dibuja" nada: genera el texto del HTML, y el navegador lo interpreta.

Por eso todo lo que ya sabes sirve: variables, bucles, arrays... para construir HTML dinámico. Por ejemplo, una lista de platos a partir de un array.

Truco

En la consola ves el HTML "en crudo", tal cual lo recibiría el navegador. Si lo abrieras en un navegador de verdad, verías una lista con viñetas.

<?php
$platos = ["Sopa de letras", "Pan de bytes", "Tarta de arrays"];
echo "<h2>Menú del día</h2>\n";
echo "<ul>\n";
foreach ($platos as $plato) {
    echo "  <li>$plato</li>\n";
}
echo "</ul>\n";
Salida
<h2>Menú del día</h2>
<ul>
  <li>Sopa de letras</li>
  <li>Pan de bytes</li>
  <li>Tarta de arrays</li>
</ul>

$_GET: datos en la URL

Una URL puede llevar datos al final, tras un ?: posada.php?nombre=Ana&edad=30. Es la query string.

PHP los mete en el array $_GET: $_GET['nombre'] vale "Ana".

$_GET es una superglobal: existe en todos los ámbitos, incluso dentro de funciones, sin global.

Cuidado

Lo más importante: todo lo que llega en $_GET es un string (o no llega). Aunque escribas edad=30, recibes "30", no el número 30. Por eso === con un número falla.

En este ejemplo simulamos la petición ?nombre=Ana&edad=30.

<?php
var_dump($_GET);
echo "Hola, ", $_GET["nombre"], "\n";
var_dump($_GET["edad"] === 30);
var_dump((int) $_GET["edad"] === 30);
Salida
array(0) {
}
Hola, 
bool(false)
bool(false)

Nunca te fíes del cliente

Los datos de una petición los controla quien la envía: pueden faltar, venir vacíos o traer cualquier cosa. La primera regla del desarrollo web: valida siempre.

  • ¿Existe? isset($_GET['x']) o, con valor por defecto, $_GET['x'] ?? 'defecto'.
  • ¿Es un número? ctype_digit($s) (solo dígitos), is_numeric($s) o filter_var($s, FILTER_VALIDATE_INT) (devuelve el int o false).
  • ¿Está en un rango razonable?

En este ejemplo, la petición es ?cantidad=abc y no trae plato.

<?php
$plato = $_GET["plato"] ?? "sopa";
$cantidad = filter_var($_GET["cantidad"] ?? "", FILTER_VALIDATE_INT);
if ($cantidad === false || $cantidad < 1) {
    echo "Cantidad no válida\n";
} else {
    echo "Marchando $cantidad de $plato\n";
}
var_dump(ctype_digit("12"), ctype_digit("12a"), ctype_digit("-3"));
Salida
Cantidad no válida
bool(true)
bool(false)
bool(false)

XSS y htmlspecialchars

Imagina que imprimes "Hola, " . $_GET['nombre'] y alguien visita tu web con ?nombre=<script>...</script>. Tu página devolvería ese <script> y el navegador lo ejecutaría: el atacante podría robar sesiones o engañar a tus usuarios. Es un ataque XSS (Cross-Site Scripting), uno de los más comunes de la web.

La defensa: escapar todo dato externo al meterlo en HTML con htmlspecialchars(). Convierte los caracteres especiales en entidades: < → &lt;, > → &gt;, " → &quot;, ' → &#039;, & → &amp;. El navegador los muestra como texto en vez de interpretarlos.

Cuidado

Regla de oro: escapa en la salida, justo al imprimir en HTML, siempre que el dato venga de fuera.

<?php
$nombre = $_GET["nombre"] ?? "forastero";
echo "<p>Hola, " . $nombre . "</p>\n";
echo "<p>Hola, " . htmlspecialchars($nombre) . "</p>\n";
Salida
<p>Hola, forastero</p>
<p>Hola, forastero</p>

Formularios y $_POST

Los formularios HTML envían datos. Con method="get" van en la URL ($_GET); con method="post" van en el cuerpo de la petición y llegan en $_POST.

Usa POST para enviar datos que cambian algo (hacer un pedido, registrarse) o que no deben quedar en la URL (contraseñas). Usa GET para consultas (buscar, filtrar).

$_SERVER['REQUEST_METHOD'] te dice cómo llegó la petición. El patrón típico: si es POST, procesar el formulario; si no, mostrarlo.

En este ejemplo se simula el envío por POST de plato=tarta.

<?php
if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $plato = htmlspecialchars($_POST["plato"] ?? "");
    echo "<p>Pedido recibido: $plato</p>\n";
} else {
    echo <<<HTML
    <form method="post">
      <input name="plato">
      <button>Pedir</button>
    </form>
    HTML;
}
Salida
<form method="post">
  <input name="plato">
  <button>Pedir</button>
</form>

Plantillas

Cuando hay mucho HTML y poco PHP, es más cómodo escribir el HTML "fuera" y abrir PHP solo donde hace falta. Para eso existe la sintaxis alternativa de las estructuras de control, con : y end...:

<?php foreach ($platos as $p): ?>
  <li><?= htmlspecialchars($p) ?></li>
<?php endforeach; ?>

También existen if: ... elseif: ... else: ... endif;, for: ... endfor; y while: ... endwhile;. Así nacieron las plantillas en PHP, y siguen siendo la base de muchos sistemas.

Truco

Detalle: PHP se "come" el salto de línea que va justo después de ?>. Por eso en la salida no aparecen líneas en blanco de más.

<?php $platos = ["Sopa", "Pan", "Tarta"]; ?>
<ul>
<?php foreach ($platos as $i => $p): ?>
  <li><?= $i + 1 ?>. <?= htmlspecialchars($p) ?></li>
<?php endforeach; ?>
</ul>
<?php if (count($platos) > 2): ?>
<p>¡Menú completo!</p>
<?php endif; ?>
Salida
<ul>
  <li>1. Sopa</li>
  <li>2. Pan</li>
  <li>3. Tarta</li>
</ul>
<p>¡Menú completo!</p>

Sin memoria: sesiones y cookies

Si cada petición empieza de cero... ¿cómo sabe una tienda online qué tienes en el carrito? ¿Cómo sigues "conectado" al cambiar de página?

  • Cookies: pequeños datos que el servidor pide al navegador que guarde (setcookie()) y que el navegador reenvía en cada petición (llegan en $_COOKIE).
  • Sesiones: PHP guarda los datos en el servidor y solo envía al navegador una cookie con un identificador aleatorio. Con session_start(), el array $_SESSION se recupera en cada petición a partir de ese identificador.
Pergamino del porque

Fíjate en la idea: como PHP olvida todo entre peticiones, cualquier dato que deba sobrevivir tiene que guardarse fuera del script: en una cookie, en la sesión (un archivo o memoria en el servidor) o en una base de datos. Ese es el siguiente gran paso en tu camino con PHP.

El ejemplo de la derecha es un contador de visitas "ingenuo": en una web real, siempre mostraría 1.

<?php
// Cada petición ejecuta el script desde cero:
$visitas = 0;
$visitas++;
echo "Eres la visita número $visitas\n";
// Para contar de verdad habría que guardar el número
// fuera del script: en $_SESSION, una cookie o una base de datos.
Salida
Eres la visita número 1

Ejercicios de PHP en la web resueltos

Intenta resolverlos antes de mirar la solución. En el juego puedes ejecutarlos y comprobarlos automáticamente.

Encargo 1. Bienvenida segura

La página de bienvenida de la Posada saluda a quien llega con ?nombre=.... Pero no todos los clientes son de fiar.

Imprime <h1>Hola, NOMBRE</h1> usando $_GET["nombre"]. Si no llega, usa forastero. Y protege la página contra XSS.

Pistas
  • $nombre = $_GET["nombre"] ?? "forastero";
  • echo "<h1>Hola, " . htmlspecialchars($nombre) . "</h1>";
Ver solución
<?php
$nombre = $_GET["nombre"] ?? "forastero";
echo "<h1>Hola, " . htmlspecialchars($nombre) . "</h1>";

Encargo 2. La comanda

La Posada recibe pedidos por formulario: plato y cantidad.

Escribe el procesador:

  • Si la petición no es POST: imprime Envía el formulario.
  • Si cantidad no es un número entero de 1 o más (o no llega): Cantidad no válida.
  • Si todo va bien: Pedido: N x PLATO (con el plato escapado con htmlspecialchars).
Pistas
  • $cantidad = filter_var($_POST["cantidad"] ?? "", FILTER_VALIDATE_INT); devuelve false si no es entero.
  • Comprueba $cantidad === false || $cantidad < 1.
Ver solución
<?php
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
    echo "Envía el formulario";
} else {
    $cantidad = filter_var($_POST["cantidad"] ?? "", FILTER_VALIDATE_INT);
    if ($cantidad === false || $cantidad < 1) {
        echo "Cantidad no válida";
    } else {
        $plato = htmlspecialchars($_POST["plato"] ?? "");
        echo "Pedido: $cantidad x $plato";
    }
}

Encargo 3. La carta de la Posada

Rex quiere la carta en HTML. Los platos y precios están en $carta (plato => precio).

Genera exactamente este HTML (precios con 2 decimales y coma decimal):

<ul>
<li>Sopa: 4,50 €</li>
<li>Pan: 1,20 €</li>
</ul>
Pistas
  • number_format($precio, 2, ",", ".") da 4,50.
  • Dentro del foreach: echo "<li>$plato: ", number_format($precio, 2, ",", "."), " €</li>\n";
Ver solución
<?php
echo "<ul>\n";
foreach ($carta as $plato => $precio) {
    echo "<li>" . htmlspecialchars($plato) . ": " . number_format($precio, 2, ",", ".") . " €</li>\n";
}
echo "</ul>\n";

Predicción 4. Todo son strings

Un cliente pide ?edad=30&vip=0. (La línea 2 simula esa petición.)

Elige la salida.

<?php
$_GET = ["edad" => "30", "vip" => "0"];
var_dump($_GET["edad"]);
var_dump($_GET["edad"] + 1);
var_dump($_GET["edad"] === 30);
echo $_GET["vip"] ? "VIP" : "normal";
  1. string(2) "30" int(31) bool(false) normal
  2. int(30) int(31) bool(true) normal
  3. string(2) "30" string(3) "301" bool(false) VIP
  4. string(2) "30" int(31) bool(false) VIP
Ver solución
string(2) "30"
int(31)
bool(false)
normal

Todo llega como string. Y "0" es falso: por eso el cliente no es VIP.

Caza de bugs 5. El agujero en la pared

La página de reseñas de la Posada muestra el comentario que llega por la URL. Un pícaro ya lo ha aprovechado.

Arregla la página para que sea segura sin cambiar el formato.

Código de partida:

<?php
$autor = $_GET["autor"] ?? "Anónimo";
$texto = $_GET["texto"] ?? "";
echo "<blockquote>" . $texto . "</blockquote>\n";
echo "<cite>" . $autor . "</cite>\n";
Pistas
  • Hay DOS datos externos. Los dos deben pasar por htmlspecialchars() al imprimirlos.
Ver solución
<?php
$autor = $_GET["autor"] ?? "Anónimo";
$texto = $_GET["texto"] ?? "";
echo "<blockquote>" . htmlspecialchars($texto) . "</blockquote>\n";
echo "<cite>" . htmlspecialchars($autor) . "</cite>\n";

Acertijo 6. El examen de la barra

Rex seca un vaso: "Antes de que te vayas, unas preguntas de la casa".

Responde a las cinco preguntas.

  1. Después de responder a una petición, ¿qué pasa con las variables del script?

    1. Se guardan para la siguiente petición
    2. Desaparecen: cada petición empieza de cero
    3. Se guardan en $_GET
    Ver respuesta

    B. Desaparecen: cada petición empieza de cero

  2. Con ?edad=30, $_GET["edad"] es...

    1. int(30)
    2. string(2) "30"
    3. float(30)
    Ver respuesta

    B. string(2) "30"

  3. ¿Para qué sirve htmlspecialchars()?

    1. Para dar color al HTML
    2. Para evitar XSS escapando caracteres especiales
    3. Para validar emails
    Ver respuesta

    B. Para evitar XSS escapando caracteres especiales

  4. ¿Qué método usarías para un formulario de registro con contraseña?

    1. GET
    2. POST
    Ver respuesta

    B. POST

  5. ¿Dónde guardarías el carrito de la compra entre peticiones?

    1. En una variable global
    2. En $_SESSION (o en una base de datos)
    3. En una constante
    Ver respuesta

    B. En $_SESSION (o en una base de datos)

Chuleta: lo esencial

  • Petición HTTP → el servidor ejecuta el script desde cero → lo impreso es la respuesta (HTML) → PHP lo olvida todo (shared-nothing).
  • Superglobales: $_GET (URL), $_POST (formulario), $_SERVER, $_COOKIE, $_SESSION. Existen en todos los ámbitos.
  • Todo llega como string (o no llega). Valida: ?? 'defecto', isset, filter_var($s, FILTER_VALIDATE_INT), ctype_digit.
  • XSS: escapa todo dato externo al imprimirlo en HTML con htmlspecialchars().
  • POST para cambios o datos sensibles; GET para consultas. $_SERVER['REQUEST_METHOD'].
  • Plantillas: <?php foreach (...): ?> ... <?php endforeach; ?>, <?= $x ?>.
  • Persistir entre peticiones: cookies, sesiones (session_start(), $_SESSION) o base de datos.