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.

1. Los datos personales nunca viajan en la URL

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

Por qué una URL no es lugar para datos personales

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.

En la API

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."""

2. Minimización: el documento de identidad no se pide al reservar

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.

3. Consentimiento comercial separado y opcional

<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 = True

Nunca se revoca automáticamente: una reserva posterior con la casilla sin marcar no borra un permiso que el huésped concedió antes.

4. Quién puede ver una reserva

Sin login hace falta otro mecanismo para que solo el titular acceda a sus datos.

El código de confirmación no es adivinable

ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"   # sin 0/O ni 1/I
CODE_LENGTH = 8

Se 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.

El código por sí solo no basta

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.

La página de confirmación posterior a la reserva

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.

Post/Redirect/Get

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.

5. Qué falta para producción

  • HTTPS obligatorio. Todo lo anterior asume tránsito cifrado. En producción hacen falta SECURE_SSL_REDIRECT, SESSION_COOKIE_SECURE y CSRF_COOKIE_SECURE.
  • DEBUG = False y SECRET_KEY fuera del control de versiones. La clave actual está en settings.py porque es la generada por startproject para desarrollo; debe rotarse y moverse a una variable de entorno antes de cualquier despliegue.
  • ALLOWED_HOSTS con 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.
⚠️ **GitHub.com Fallback** ⚠️