Seguridad de los datos personales - MiguelAngelMarinMejia1/ArquitecturaDeSoftware-Proyecto GitHub Wiki
Reservar sin cuenta obliga a resolver dos preguntas que el login normalmente contesta solo: por dónde viajan los datos del huésped, y quién puede volver a verlos después.
El flujo de reserva reparte la información en dos canales según su naturaleza:
| Paso | Transporte | Qué lleva |
|---|---|---|
| Buscar disponibilidad | Query string | fechas, adultos, niños, nº de habitaciones |
| Elegir habitación | Query string | identificadores de habitación |
| Datos del huésped | Cuerpo del POST | nombre, apellido, correo, teléfono, país |
| Confirmar | POST + redirect | se persiste y se redirige |
Una cadena de consulta parece inofensiva, pero se replica en más sitios de los que se supone:
- Historial del navegador, accesible a cualquiera que use el equipo después.
- Registros de acceso del servidor web, de los proxies intermedios y del API Gateway. Un correo electrónico en un log queda archivado durante meses en sistemas que nadie considera sensibles.
-
Cabecera
Referer: al hacer clic en cualquier enlace externo, o al cargar un recurso de un tercero (analítica, CDN, un mapa embebido), el navegador envía la URL completa a ese tercero. - Enlaces compartidos: quien copia la dirección para enseñar "mira qué habitación" comparte también lo que lleve dentro.
- Marcadores y sincronización entre dispositivos.
Está catalogado por OWASP como Information Exposure Through Query Strings in URL (CWE-598).
Las fechas y el número de huéspedes, en cambio, no identifican a nadie. Mantenerlos en la URL tiene ventajas reales: los resultados son compartibles, recargables y cacheables, y el diseño se mantiene sin estado, que es lo que sostiene la estrategia de API Gateway.
El endpoint de consulta es POST /api/v1/reservations/lookup/ y no un GET con parámetros,
precisamente porque necesita el correo del huésped:
class ReservationLookupView(APIView):
"""Modelado como POST para que el correo quede en el cuerpo y no en la URL."""Customer.document_id es anulable y queda vacío en todo el flujo web.
El motivo es que no hace falta para reservar. En un hotel real el documento se solicita en el check-in presencial, cuando existe una obligación legal de registro de huéspedes y alguien lo verifica físicamente. Pedirlo por internet sería recolectar el dato más sensible del sistema para no usarlo.
Esto aplica el principio de minimización de datos de la Ley 1581 de 2012 (habeas data, Colombia): recolectar únicamente lo necesario para la finalidad declarada. El dato que no se guarda es el único que no se puede filtrar.
Por la misma razón, CustomerReadSerializer no expone document_id en ninguna respuesta.
<label for="marketing-consent">
<input type="checkbox" id="marketing-consent" name="marketing_consent" value="1">
Quiero recibir ofertas y novedades de Hotel MDJ
</label>La casilla está desmarcada por defecto, es independiente del resto del formulario y no condiciona la reserva. En el servicio, el consentimiento solo se concede:
if data.get("marketing_consent"):
customer.marketing_consent = TrueNunca se revoca automáticamente: una reserva posterior con la casilla sin marcar no borra un permiso que el huésped concedió antes.
Sin login hace falta otro mecanismo para que solo el titular acceda a sus datos.
ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ" # sin 0/O ni 1/I
CODE_LENGTH = 8Se genera con secrets, no con random, y no guarda relación con el identificador de la fila. Si
la URL fuera /reservas/1/, cualquiera podría recorrer las reservas ajenas cambiando el número. El
alfabeto excluye caracteres que se confunden al dictarse por teléfono.
Con 32 caracteres y 8 posiciones hay 32⁸ ≈ 1,1 × 10¹² combinaciones.
Consultar una reserva exige código de confirmación más correo electrónico, igual que en Booking o Marriott:
def find_reservation(confirmation_code: str, email: str):
return Reservation.objects.filter(
confirmation_code__iexact=...,
customer__email__iexact=...,
).first()Un código filtrado —en una captura de pantalla, un registro de servidor, un correo reenviado— no alcanza para exponer los datos de nadie.
Justo después de reservar sería absurdo pedirle el correo a quien acaba de escribirlo. Se resuelve con un testigo en la sesión:
if request.session.get(SESSION_RECENT_RESERVATION) != code:
return redirect("web:reservation-lookup")Quien acaba de reservar ve su confirmación. Quien llegue a esa misma URL más tarde, desde otro navegador o desde un enlace compartido, es enviado al formulario de consulta. Está verificado en las pruebas de flujo: un cliente distinto recibe una redirección, no los datos.
Tras crear la reserva se responde con una redirección en lugar de renderizar directamente. Recargar la página de confirmación no vuelve a crear la reserva ni reenvía el pago.
-
HTTPS obligatorio. Todo lo anterior asume tránsito cifrado. En producción hacen falta
SECURE_SSL_REDIRECT,SESSION_COOKIE_SECUREyCSRF_COOKIE_SECURE. -
DEBUG = FalseySECRET_KEYfuera del control de versiones. La clave actual está ensettings.pyporque es la generada porstartprojectpara desarrollo; debe rotarse y moverse a una variable de entorno antes de cualquier despliegue. -
ALLOWED_HOSTScon los dominios reales. - Límite de intentos en el formulario de consulta, para impedir la enumeración por fuerza bruta de códigos.
- Política de retención: definir cuánto tiempo se conservan los perfiles de huéspedes que nunca volvieron.
- Datos de tarjeta: la pasarela simulada no captura ninguno, y esa decisión debe mantenerse. La integración real debe usar tokenización del proveedor para que el número de tarjeta no pase jamás por este servidor.