Independiente / Laboratorio
Cabeceras HTTP: del hallazgo a la verificación
Laboratorio local con Python para observar una respuesta HTTP, aplicar cabeceras de seguridad y verificar sus valores sin depender de servicios externos.
En este writeup
Contexto y objetivo
Un análisis web detecta cabeceras de seguridad ausentes. Antes de convertir esa observación en una conclusión, necesitamos entender qué respuesta recibimos, qué controla cada cabecera y cómo verificar el cambio.
Crearemos una página mínima y compararemos dos configuraciones del mismo servidor. El objetivo es practicar el ciclo observar, modificar y comprobar, sin instalar dependencias ni examinar servicios de terceros.
Necesitas Python 3, una carpeta vacía y dos terminales abiertas en ella. El servidor se enlaza explícitamente a la interfaz de bucle local y devuelve HTML definido en el código: no publica los archivos de tu carpeta. La biblioteca http.server de Python ofrece las clases del ejemplo y está pensada aquí como herramienta de laboratorio.
Preparar el laboratorio
Guarda el siguiente archivo como servidor.py:
import argparse
from http.server import BaseHTTPRequestHandler, HTTPServer
parser = argparse.ArgumentParser()
parser.add_argument("--headers", action="store_true")
args = parser.parse_args()
SECURITY_HEADERS = {
"Content-Security-Policy": (
"default-src 'none'; base-uri 'none'; "
"frame-ancestors 'none'; form-action 'none'"
),
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
}
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = (
'<!doctype html><html lang="es"><head>'
'<meta charset="utf-8"><title>Laboratorio local</title>'
'</head><body><h1>PurpSec</h1>'
'<p>Respuesta local de demostracion.</p></body></html>'
).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
if args.headers:
for name, value in SECURITY_HEADERS.items():
self.send_header(name, value)
self.end_headers()
self.wfile.write(body)
with HTTPServer(("127.0.0.1", 8765), Handler) as server:
print("Laboratorio: http://127.0.0.1:8765/", flush=True)
print("Cabeceras activadas:", args.headers, flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
print("Servidor detenido.")
En la primera terminal, inicia la configuración inicial:
python servidor.py
El proceso permanece abierto mientras atiende peticiones. Puedes visitar la dirección que imprime. Si el puerto está ocupado, detén tu ejecución anterior del laboratorio antes de repetir; no hace falta cambiar servicios ajenos al ejercicio.
Observar la respuesta
Guarda como comprobar.py este cliente. Desactiva el uso de proxies del entorno para que la solicitud vaya directamente al servidor local:
from urllib.request import ProxyHandler, build_opener
expected = {
"Content-Security-Policy": (
"default-src 'none'; base-uri 'none'; "
"frame-ancestors 'none'; form-action 'none'"
),
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
}
client = build_opener(ProxyHandler({}))
with client.open("http://127.0.0.1:8765/", timeout=3) as response:
print("HTTP", response.status)
for name, wanted in expected.items():
actual = response.headers.get(name)
status = "OK" if actual == wanted else "REVISAR"
print(status, name, "=", actual or "AUSENTE")
Ejecuta en la segunda terminal:
python comprobar.py
El resultado inicial será HTTP 200 y tres líneas REVISAR con el valor AUSENTE. La respuesta funciona, pero no comunica las tres políticas seleccionadas. El hallazgo describe esa respuesta concreta; no demuestra por sí solo una explotación.
En sistemas que utilizan python3 o py, sustituye python por ese ejecutable en todos los comandos.
Aplicar el cambio y verificar
Detén el servidor con Ctrl+C en la primera terminal y vuelve a iniciarlo:
python servidor.py --headers
Repite python comprobar.py en la segunda terminal. Ahora deben aparecer tres resultados OK con los valores configurados. Mantuvimos la página y el cliente: la diferencia observada procede del cambio de cabeceras.
| Cabecera | Interpretación en este laboratorio |
|---|---|
Content-Security-Policy |
La página no necesita recursos externos, formularios ni inclusión en marcos. |
X-Content-Type-Options |
nosniff indica que debe respetarse el tipo de contenido declarado. |
Referrer-Policy |
no-referrer evita enviar información de referencia en las solicitudes a las que se aplica. |
La referencia de CSP explica las directivas. frame-ancestors y form-action tienen controles propios, por eso aparecen expresamente en nuestra política. Esta configuración sería demasiado restrictiva para muchas aplicaciones reales.
El alcance de nosniff depende del contexto de la respuesta, como detalla X-Content-Type-Options. Una cabecera presente no sustituye la validación de datos ni corrige errores de autorización.
Resultado e implicación defensiva
Hemos verificado que el servidor emite los valores esperados. El cliente comprueba las cabeceras; no simula la aplicación de las políticas por un navegador. La consola del navegador y pruebas de comportamiento aportan esa segunda capa de evidencia.
En un proyecto real, el siguiente paso sería inventariar recursos legítimos, diseñar una CSP compatible y revisar también respuestas de error y redirecciones. Una modificación debe conservar las funciones necesarias de la aplicación.
Al terminar, pulsa Ctrl+C en la terminal del servidor. El aprendizaje central es documentar una observación reproducible, su corrección y los límites de la verificación.