Volver al laboratorio ~/iphobiuss/casos/sesiones-seguras

Cerrar una sesión debe revocar su acceso

Comprobar el token después del cierre, del cambio de contraseña y de su fecha límite.

En este caso
Problema
Una API reconoce el token, pero no comprueba si la sesión sigue vigente.
Causa raíz
La validación ignora revocación, cambio de contraseña y fecha de caducidad.
Decisión
Comprobar el estado del servidor en cada petición y definir qué sesiones invalida cada operación.
Evidencia
24 escenarios, dos modos y 154 intercambios HTTP reales con Alice y Bruno.
Resultado
El modo corregido rechaza los tokens invalidados y conserva los accesos legítimos de las sesiones que deben continuar.

Reconocer un token no basta

Construí esta API local para comparar una sesión conocida con una sesión vigente. El fallo del modo vulnerable es deliberado: no procede de un cliente ni de un incidente real.

Alice y Bruno tienen un portátil y un teléfono cada uno. Sus cuatro tokens iniciales identifican sesiones distintas. GET /me devuelve un marcador propio de cada identidad; las operaciones de sesión se ejecutan mediante POST. Las contraseñas y tokens son públicos y sintéticos.

  • Cerrar sesión debe invalidar ese token, aunque el cliente lo conserve.
  • Cambiar la contraseña debe invalidar las sesiones anteriores de esa identidad, según la política elegida.
  • Revocar un dispositivo debe afectar únicamente a ese dispositivo propio.
  • Una sesión caducada debe rechazarse en la siguiente petición protegida.
  • Las otras identidades y las sesiones que siguen vigentes deben conservar su acceso.

La batería envía HTTP real a 127.0.0.1. Inicia un servidor nuevo por escenario y modo, y mantiene el estado entre pasos. La caducidad usa un reloj controlado dentro del proceso: se prueban las fronteras temporales sin esperar un minuto ni simular las respuestas HTTP.

Seguir el token después de cerrar sesión

  1. 1. Confirmar la identidad. El token del portátil de Alice obtiene 200 y el marcador SYNTHETIC_ALICE.
  2. 2. Solicitar el cierre. POST /logout devuelve 200 y signed_out: true en ambos modos. Esa respuesta por sí sola no acredita revocación.
  3. 3. Reutilizar exactamente el token anterior. El modo vulnerable todavía permite GET /me. El corregido devuelve 401 sin identidad ni marcador.
  4. 4. Comprobar el alcance. El teléfono de Alice y el portátil de Bruno siguen obteniendo 200: el cierre afecta a la sesión seleccionada.

Abrir las cinco peticiones y respuestas de Alice. Repetir el control con Bruno.

El retest conserva el token de forma intencional. Borrarlo del cliente impediría observar si el servidor aún lo acepta. En esta API se transporta como Bearer; el caso no ensaya cookies de navegador.

Decidir qué debe dejar de funcionar y qué debe continuar

Cierre de sesión
Se marca como revocada la sesión actual. Las demás siguen vigentes. Ver alcance del cierre.
Contraseña
Se exige la contraseña actual y se incrementa la generación de la identidad. Todas sus sesiones previas, incluida la actual, deben dejar de funcionar. El usuario inicia sesión de nuevo; la contraseña anterior se rechaza y la nueva emite una sesión utilizable. Ver los siete pasos.
Dispositivo
El servidor busca su propietario antes de revocarlo. Un identificador ajeno responde igual que uno inexistente y su token sigue funcionando. Ver el rechazo y el control de Bruno.
Caducidad
La vida absoluta es de 60 segundos: a los 59 se permite el acceso; a los 60 se rechaza. Un login posterior obtiene una sesión nueva con su propio límite. Ver el límite inicial y el límite de la sesión nueva.
Orden de ejecución
Un lock protege conjuntamente la validación y la mutación en este proceso. Así una revocación no puede intercalarse entre esos dos pasos. Esto no demuestra coordinación entre réplicas, cachés o bases de datos distribuidas.

Estas son decisiones explícitas del laboratorio. Otra aplicación podría conservar la sesión actual tras un cambio de contraseña, pero tendría que definir y probar esa política. Aquí se eligió invalidarlas todas para que el alcance del cambio sea inequívoco.

Validar el estado en cada petición

Este diff se extrae de las funciones que ejecuta el archivo descargable. Ambos modos realizan las mismas mutaciones; la diferencia está en qué estado aceptan al autenticar la siguiente petición.

Políticas ejecutadas por el laboratorio
--- lab.py: sesión vulnerable
+++ lab.py: sesión corregida
@@ -1,2 +1,4 @@
-def valid_vulnerable(session, user, now):
-    return session is not None
+def valid_fixed(session, user, now):
+    return (session is not None and not session["revoked"]
+            and session["generation"] == user["generation"]
+            and now < session["expires_at"])

Un token conocido puede estar revocado, pertenecer a una generación anterior o haber caducado. El modo corregido comprueba las tres condiciones antes de devolver información o realizar otra mutación.

Por ejemplo, una sesión cerrada tampoco puede revocar el teléfono que sigue activo. Ver el intento y la comprobación posterior.

Retest: respuestas completas y controles negativos

154/154 intercambios coinciden con sus expectativas. Los 24 escenarios se ejecutan en ambos modos. En la variante vulnerable, parte de las respuestas esperadas demuestra el defecto; un PASS indica reproducción fiel de la batería.

También se rechazan tokens inventados, autenticación duplicada, JSON inválido, cambios con contraseña incorrecta e identidades declaradas por el cliente. Los controles posteriores comprueban que el rechazo conserva las sesiones que no debían cambiar.

Los desplegables funcionan sin JavaScript. Cada transcripción conserva las cabeceras de aplicación, el estado y el cuerpo completo observados. El JSON incluye las expectativas y SHA-256 de cada cuerpo. Se omiten Host, puerto efímero, Date y Server; no se presenta como captura de red.

Alice: cerrar sesión invalida solo el token actual
Vulnerable: 200 → 200 → 200 → 200 → 200 · Corregido: 200 → 200 → 401 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Alice: cerrar sesión invalida solo el token actual
Intercambio 1 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 2 · reloj controlado: 0 s
POST /logout
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 19
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"signed_out":true}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 5 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Corregido: Alice: cerrar sesión invalida solo el token actual
Intercambio 1 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 2 · reloj controlado: 0 s
POST /logout
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 19
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"signed_out":true}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 5 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Alice: cambiar la contraseña invalida sus dos dispositivos
Vulnerable: 200 → 200 → 200 → 200 → 401 → 200 → 200 · Corregido: 200 → 401 → 401 → 200 → 401 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Alice: cambiar la contraseña invalida sus dos dispositivos
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 77

{"current_password":"demo-alice-initial","new_password":"demo-alice-changed"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 59
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"changed":true,"scope":"all_sessions_of_current_identity"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 5 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-initial"}

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 6 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-changed"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"alice-new-1","token":"lab-alice-issued-1","ttl_seconds":60}

Intercambio 7 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Alice: cambiar la contraseña invalida sus dos dispositivos
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 77

{"current_password":"demo-alice-initial","new_password":"demo-alice-changed"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 59
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"changed":true,"scope":"all_sessions_of_current_identity"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 5 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-initial"}

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 6 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-changed"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"alice-new-1","token":"lab-alice-issued-1","ttl_seconds":60}

Intercambio 7 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Alice: revocar el teléfono conserva las otras sesiones
Vulnerable: 200 → 200 → 200 → 200 · Corregido: 200 → 401 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Alice: revocar el teléfono conserva las otras sesiones
Intercambio 1 · reloj controlado: 0 s
POST /sessions/alice-phone/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 33
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"revoked_session":"alice-phone"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Corregido: Alice: revocar el teléfono conserva las otras sesiones
Intercambio 1 · reloj controlado: 0 s
POST /sessions/alice-phone/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 33
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"revoked_session":"alice-phone"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Alice: no puede revocar el dispositivo ajeno
Vulnerable: 404 → 200 → 200 · Corregido: 404 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Alice: no puede revocar el dispositivo ajeno
Intercambio 1 · reloj controlado: 0 s
POST /sessions/bruno-phone/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 404
Content-Type: application/json; charset=utf-8
Content-Length: 21
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"not_found"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Alice: no puede revocar el dispositivo ajeno
Intercambio 1 · reloj controlado: 0 s
POST /sessions/bruno-phone/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 404
Content-Type: application/json; charset=utf-8
Content-Length: 21
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"not_found"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Alice: activo antes del límite, rechazado al cumplirse 60 segundos
Vulnerable: 200 → 200 → 200 → 200 · Corregido: 200 → 401 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Alice: activo antes del límite, rechazado al cumplirse 60 segundos
Intercambio 1 · reloj controlado: 59 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 2 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 60 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-initial"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"alice-new-1","token":"lab-alice-issued-1","ttl_seconds":60}

Intercambio 4 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-alice-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Alice: activo antes del límite, rechazado al cumplirse 60 segundos
Intercambio 1 · reloj controlado: 59 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 2 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 60 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-initial"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"alice-new-1","token":"lab-alice-issued-1","ttl_seconds":60}

Intercambio 4 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-alice-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Bruno: cerrar sesión invalida solo el token actual
Vulnerable: 200 → 200 → 200 → 200 → 200 · Corregido: 200 → 200 → 401 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Bruno: cerrar sesión invalida solo el token actual
Intercambio 1 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 2 · reloj controlado: 0 s
POST /logout
Authorization: Bearer lab-bruno-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 19
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"signed_out":true}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 5 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Bruno: cerrar sesión invalida solo el token actual
Intercambio 1 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 2 · reloj controlado: 0 s
POST /logout
Authorization: Bearer lab-bruno-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 19
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"signed_out":true}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 5 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Bruno: cambiar la contraseña invalida sus dos dispositivos
Vulnerable: 200 → 200 → 200 → 200 → 401 → 200 → 200 · Corregido: 200 → 401 → 401 → 200 → 401 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Bruno: cambiar la contraseña invalida sus dos dispositivos
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-bruno-laptop
Content-Type: application/json
Content-Length: 77

{"current_password":"demo-bruno-initial","new_password":"demo-bruno-changed"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 59
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"changed":true,"scope":"all_sessions_of_current_identity"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 5 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"bruno","password":"demo-bruno-initial"}

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 6 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"bruno","password":"demo-bruno-changed"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"bruno-new-1","token":"lab-bruno-issued-1","ttl_seconds":60}

Intercambio 7 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Corregido: Bruno: cambiar la contraseña invalida sus dos dispositivos
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-bruno-laptop
Content-Type: application/json
Content-Length: 77

{"current_password":"demo-bruno-initial","new_password":"demo-bruno-changed"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 59
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"changed":true,"scope":"all_sessions_of_current_identity"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 5 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"bruno","password":"demo-bruno-initial"}

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 6 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"bruno","password":"demo-bruno-changed"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"bruno-new-1","token":"lab-bruno-issued-1","ttl_seconds":60}

Intercambio 7 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Bruno: revocar el teléfono conserva las otras sesiones
Vulnerable: 200 → 200 → 200 → 200 · Corregido: 200 → 401 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Bruno: revocar el teléfono conserva las otras sesiones
Intercambio 1 · reloj controlado: 0 s
POST /sessions/bruno-phone/revoke
Authorization: Bearer lab-bruno-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 33
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"revoked_session":"bruno-phone"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Bruno: revocar el teléfono conserva las otras sesiones
Intercambio 1 · reloj controlado: 0 s
POST /sessions/bruno-phone/revoke
Authorization: Bearer lab-bruno-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 33
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"revoked_session":"bruno-phone"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 4 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Bruno: no puede revocar el dispositivo ajeno
Vulnerable: 404 → 200 → 200 · Corregido: 404 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Bruno: no puede revocar el dispositivo ajeno
Intercambio 1 · reloj controlado: 0 s
POST /sessions/alice-phone/revoke
Authorization: Bearer lab-bruno-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 404
Content-Type: application/json; charset=utf-8
Content-Length: 21
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"not_found"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Corregido: Bruno: no puede revocar el dispositivo ajeno
Intercambio 1 · reloj controlado: 0 s
POST /sessions/alice-phone/revoke
Authorization: Bearer lab-bruno-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 404
Content-Type: application/json; charset=utf-8
Content-Length: 21
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"not_found"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Bruno: activo antes del límite, rechazado al cumplirse 60 segundos
Vulnerable: 200 → 200 → 200 → 200 · Corregido: 200 → 401 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Bruno: activo antes del límite, rechazado al cumplirse 60 segundos
Intercambio 1 · reloj controlado: 59 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 2 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 3 · reloj controlado: 60 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"bruno","password":"demo-bruno-initial"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"bruno-new-1","token":"lab-bruno-issued-1","ttl_seconds":60}

Intercambio 4 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-bruno-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Corregido: Bruno: activo antes del límite, rechazado al cumplirse 60 segundos
Intercambio 1 · reloj controlado: 59 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}

Intercambio 2 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 60 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"bruno","password":"demo-bruno-initial"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"bruno-new-1","token":"lab-bruno-issued-1","ttl_seconds":60}

Intercambio 4 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-bruno-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Una contraseña actual incorrecta no cambia ni invalida nada
Vulnerable: 403 → 200 → 200 → 200 · Corregido: 403 → 200 → 200 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Una contraseña actual incorrecta no cambia ni invalida nada
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 63

{"current_password":"incorrect-demo","new_password":"demo-new"}

Estado HTTP observado: 403
Content-Type: application/json; charset=utf-8
Content-Length: 37
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"current_password_mismatch"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 4 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-initial"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"alice-new-1","token":"lab-alice-issued-1","ttl_seconds":60}
Corregido: Una contraseña actual incorrecta no cambia ni invalida nada
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 63

{"current_password":"incorrect-demo","new_password":"demo-new"}

Estado HTTP observado: 403
Content-Type: application/json; charset=utf-8
Content-Length: 37
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"current_password_mismatch"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 4 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-initial"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"alice-new-1","token":"lab-alice-issued-1","ttl_seconds":60}
Rechazar una contraseña idéntica conserva la sesión
Vulnerable: 400 → 200 · Corregido: 400 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Rechazar una contraseña idéntica conserva la sesión
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 77

{"current_password":"demo-alice-initial","new_password":"demo-alice-initial"}

Estado HTTP observado: 400
Content-Type: application/json; charset=utf-8
Content-Length: 30
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"password_unchanged"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Rechazar una contraseña idéntica conserva la sesión
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 77

{"current_password":"demo-alice-initial","new_password":"demo-alice-initial"}

Estado HTTP observado: 400
Content-Type: application/json; charset=utf-8
Content-Length: 30
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"password_unchanged"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Una solicitud anónima no obtiene identidad
Vulnerable: 401 · Corregido: 401

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Una solicitud anónima no obtiene identidad
Intercambio 1 · reloj controlado: 0 s
GET /me

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}
Corregido: Una solicitud anónima no obtiene identidad
Intercambio 1 · reloj controlado: 0 s
GET /me

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}
Un token inventado no obtiene identidad
Vulnerable: 401 · Corregido: 401

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Un token inventado no obtiene identidad
Intercambio 1 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-unknown

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}
Corregido: Un token inventado no obtiene identidad
Intercambio 1 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-unknown

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}
Dos cabeceras de autenticación se rechazan
Vulnerable: 401 · Corregido: 401

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Dos cabeceras de autenticación se rechazan
Intercambio 1 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}
Corregido: Dos cabeceras de autenticación se rechazan
Intercambio 1 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop
Authorization: Bearer lab-bruno-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}
Una cabecera declarada por el cliente no cambia al propietario
Vulnerable: 404 → 200 · Corregido: 404 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Una cabecera declarada por el cliente no cambia al propietario
Intercambio 1 · reloj controlado: 0 s
POST /sessions/bruno-phone/revoke
Authorization: Bearer lab-alice-laptop
X-Lab-User: bruno
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 404
Content-Type: application/json; charset=utf-8
Content-Length: 21
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"not_found"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Corregido: Una cabecera declarada por el cliente no cambia al propietario
Intercambio 1 · reloj controlado: 0 s
POST /sessions/bruno-phone/revoke
Authorization: Bearer lab-alice-laptop
X-Lab-User: bruno
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 404
Content-Type: application/json; charset=utf-8
Content-Length: 21
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"not_found"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-bruno-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"bruno","marker":"SYNTHETIC_BRUNO"}
Un identificador inexistente responde igual que uno ajeno
Vulnerable: 404 → 200 · Corregido: 404 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Un identificador inexistente responde igual que uno ajeno
Intercambio 1 · reloj controlado: 0 s
POST /sessions/unknown-device/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 404
Content-Type: application/json; charset=utf-8
Content-Length: 21
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"not_found"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Un identificador inexistente responde igual que uno ajeno
Intercambio 1 · reloj controlado: 0 s
POST /sessions/unknown-device/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 404
Content-Type: application/json; charset=utf-8
Content-Length: 21
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"not_found"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Un JSON inválido no cambia la contraseña ni la sesión
Vulnerable: 400 → 200 · Corregido: 400 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Un JSON inválido no cambia la contraseña ni la sesión
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 1

{

Estado HTTP observado: 400
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"invalid_body"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Un JSON inválido no cambia la contraseña ni la sesión
Intercambio 1 · reloj controlado: 0 s
POST /password
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 1

{

Estado HTTP observado: 400
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"invalid_body"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Login rechaza campos adicionales y no adopta un token del cliente
Vulnerable: 400 → 401 → 200 · Corregido: 400 → 401 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Login rechaza campos adicionales y no adopta un token del cliente
Intercambio 1 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 75

{"user":"alice","password":"demo-alice-initial","token":"chosen-by-client"}

Estado HTTP observado: 400
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"invalid_body"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer chosen-by-client

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Login rechaza campos adicionales y no adopta un token del cliente
Intercambio 1 · reloj controlado: 0 s
POST /login
Content-Type: application/json
Content-Length: 75

{"user":"alice","password":"demo-alice-initial","token":"chosen-by-client"}

Estado HTTP observado: 400
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"invalid_body"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer chosen-by-client

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
GET no puede cerrar una sesión
Vulnerable: 405 → 200 · Corregido: 405 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: GET no puede cerrar una sesión
Intercambio 1 · reloj controlado: 0 s
GET /logout
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 405
Content-Type: application/json; charset=utf-8
Content-Length: 30
Cache-Control: no-store
X-Content-Type-Options: nosniff
Allow: POST

{"error":"method_not_allowed"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: GET no puede cerrar una sesión
Intercambio 1 · reloj controlado: 0 s
GET /logout
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 405
Content-Type: application/json; charset=utf-8
Content-Length: 30
Cache-Control: no-store
X-Content-Type-Options: nosniff
Allow: POST

{"error":"method_not_allowed"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Una sesión revocada no puede revocar otro dispositivo
Vulnerable: 200 → 200 → 200 · Corregido: 200 → 401 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Una sesión revocada no puede revocar otro dispositivo
Intercambio 1 · reloj controlado: 0 s
POST /logout
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 19
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"signed_out":true}

Intercambio 2 · reloj controlado: 0 s
POST /sessions/alice-phone/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 33
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"revoked_session":"alice-phone"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Una sesión revocada no puede revocar otro dispositivo
Intercambio 1 · reloj controlado: 0 s
POST /logout
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 19
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"signed_out":true}

Intercambio 2 · reloj controlado: 0 s
POST /sessions/alice-phone/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Revocar el dispositivo actual invalida su token
Vulnerable: 200 → 200 → 200 · Corregido: 200 → 401 → 200

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Revocar el dispositivo actual invalida su token
Intercambio 1 · reloj controlado: 0 s
POST /sessions/alice-laptop/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 34
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"revoked_session":"alice-laptop"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Revocar el dispositivo actual invalida su token
Intercambio 1 · reloj controlado: 0 s
POST /sessions/alice-laptop/revoke
Authorization: Bearer lab-alice-laptop
Content-Type: application/json
Content-Length: 2

{}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 34
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"revoked_session":"alice-laptop"}

Intercambio 2 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Intercambio 3 · reloj controlado: 0 s
GET /me
Authorization: Bearer lab-alice-phone

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Una sesión nueva también respeta su propio límite de vida
Vulnerable: 200 → 200 → 200 · Corregido: 200 → 200 → 401

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: Una sesión nueva también respeta su propio límite de vida
Intercambio 1 · reloj controlado: 30 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-initial"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"alice-new-1","token":"lab-alice-issued-1","ttl_seconds":60}

Intercambio 2 · reloj controlado: 89 s
GET /me
Authorization: Bearer lab-alice-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 90 s
GET /me
Authorization: Bearer lab-alice-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: Una sesión nueva también respeta su propio límite de vida
Intercambio 1 · reloj controlado: 30 s
POST /login
Content-Type: application/json
Content-Length: 48

{"user":"alice","password":"demo-alice-initial"}

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 74
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"session_id":"alice-new-1","token":"lab-alice-issued-1","ttl_seconds":60}

Intercambio 2 · reloj controlado: 89 s
GET /me
Authorization: Bearer lab-alice-issued-1

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}

Intercambio 3 · reloj controlado: 90 s
GET /me
Authorization: Bearer lab-alice-issued-1

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}
El reloj no se controla con un parámetro HTTP
Vulnerable: 400 → 200 · Corregido: 400 → 401

Las flechas unen peticiones sucesivas del mismo escenario. Cada modo parte de datos nuevos.

Vulnerable: El reloj no se controla con un parámetro HTTP
Intercambio 1 · reloj controlado: 60 s
GET /me?clock=0
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 400
Content-Type: application/json; charset=utf-8
Content-Length: 31
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"query_not_supported"}

Intercambio 2 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 200
Content-Type: application/json; charset=utf-8
Content-Length: 47
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"identity":"alice","marker":"SYNTHETIC_ALICE"}
Corregido: El reloj no se controla con un parámetro HTTP
Intercambio 1 · reloj controlado: 60 s
GET /me?clock=0
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 400
Content-Type: application/json; charset=utf-8
Content-Length: 31
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"query_not_supported"}

Intercambio 2 · reloj controlado: 60 s
GET /me
Authorization: Bearer lab-alice-laptop

Estado HTTP observado: 401
Content-Type: application/json; charset=utf-8
Content-Length: 24
Cache-Control: no-store
X-Content-Type-Options: nosniff

{"error":"unauthorized"}

Repetir la ejecución desde el código entregado

Python 3.10 o posterior, sin dependencias. Descomprime el ZIP y ejecuta desde su carpeta:

Verificar toda la evidencia
python3 lab.py --verify resultados.json

Salida esperada: PASS: 154/154 HTTP checks; complete evidence identical.

El ZIP incluye código, escenarios con expectativas, resultados, instrucciones, Dockerfile y manifiesto SHA-256. Cualquier diferencia en la evidencia hace fallar la comprobación.

Explorar el cierre manualmente

Inicia el servidor y sigue los comandos de INSTRUCCIONES.txt. En este modo se usa tiempo monotónico real; las sesiones iniciales caducan tras 60 segundos.

Servidor interactivo solo en loopback
python3 lab.py --serve fixed --port 8766

La descarga explica cómo repetir con --serve vulnerable, iniciar una sesión nueva y comparar el mismo token antes y después del cierre.

Repetir la batería en Docker

El contenedor ejecuta cliente y servidor como usuario no root, sin publicar puertos. La construcción descarga la imagen base; la ejecución no necesita red externa.

Construir y ejecutar
docker build -t iphobiuss-session-security:1.0.0 .
docker run --rm --network none --read-only --cap-drop ALL \
  --security-opt no-new-privileges --pids-limit 64 \
  --memory 128m --cpus 1 iphobiuss-session-security:1.0.0

Conclusión y límites de la evidencia

La comprobación del estado de sesión bloquea los tokens invalidados en los escenarios publicados. El resultado combina rechazos y controles positivos: también verifica que otras identidades y las sesiones legítimas sigan funcionando.

El servidor es una demostración: HTTP sin TLS, datos en memoria, contraseñas sintéticas en texto plano y tokens deterministas públicos. No implementa rate limiting, cookies, CSRF, refresh tokens, MFA, OIDC ni almacenamiento distribuido. La duración de 60 segundos sirve para la prueba y no es una recomendación universal de configuración.

La prueba automática de caducidad avanza un reloj controlado; no mide precisión temporal, carreras distribuidas ni retrasos de caché. Tampoco acredita una evaluación humana o una auditoría de una aplicación en producción. Docker reproduce la misma cobertura.

Una ampliación posterior podría introducir persistencia y dos réplicas para comprobar revocaciones compartidas. Necesitaría escenarios y evidencia nuevos.