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:
- El navegador envía una petición HTTP a un servidor: "dame
/posada.php?plato=sopa". - El servidor web (Apache, Nginx...) ve que es un archivo
.phpy se lo pasa al intérprete de PHP. - PHP ejecuta el script desde el principio, con los datos de la petición ya preparados en unas variables especiales (
$_GET,$_POST,$_SERVER...). - Todo lo que el script imprime con
echo(y el HTML de fuera de<?php) forma la respuesta, que vuelve al navegador. - El navegador dibuja ese HTML.
- PHP lo olvida todo: variables, arrays, objetos... desaparecen.
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><!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.
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";<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.
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);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)ofilter_var($s, FILTER_VALIDATE_INT)(devuelve el int ofalse). - ¿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"));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: < → <, > → >, " → ", ' → ', & → &. El navegador los muestra como texto en vez de interpretarlos.
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";<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;
}<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.
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; ?><ul> <li>1. Sopa</li> <li>2. Pan</li> <li>3. Tarta</li> </ul> <p>¡Menú completo!</p>
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
cantidadno 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 conhtmlspecialchars).
Pistas
$cantidad = filter_var($_POST["cantidad"] ?? "", FILTER_VALIDATE_INT);devuelvefalsesi 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, ",", ".")da4,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";string(2) "30" int(31) bool(false) normalint(30) int(31) bool(true) normalstring(2) "30" string(3) "301" bool(false) VIPstring(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.
Después de responder a una petición, ¿qué pasa con las variables del script?
- Se guardan para la siguiente petición
- Desaparecen: cada petición empieza de cero
- Se guardan en
$_GET
Ver respuesta
B. Desaparecen: cada petición empieza de cero
Con
?edad=30,$_GET["edad"]es...int(30)string(2) "30"float(30)
Ver respuesta
B.
string(2) "30"¿Para qué sirve
htmlspecialchars()?- Para dar color al HTML
- Para evitar XSS escapando caracteres especiales
- Para validar emails
Ver respuesta
B. Para evitar XSS escapando caracteres especiales
¿Qué método usarías para un formulario de registro con contraseña?
- GET
- POST
Ver respuesta
B. POST
¿Dónde guardarías el carrito de la compra entre peticiones?
- En una variable global
- En
$_SESSION(o en una base de datos) - 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.