study.plynte.com
@franguh
Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)

16. Autenticación, autorización y sesiones#

Autenticar responde a la pregunta “¿qué identidad presenta esta solicitud?”; autorizar responde a “¿esa identidad verificada tiene permiso para realizar esta acción concreta sobre este recurso específico?”. Confundir ambas preguntas es una de las fallas arquitectónicas y de seguridad más costosas de la industria: estar válidamente conectado a un sistema no convierte al usuario en propietario ni le otorga acceso a cada registro numérico accesible en la base de datos.

Objetivo#

Al finalizar esta lección podrás:

  • Diseñar e implementar decisiones de autorización por recurso (Resource-Level Authorization) en el servidor, garantizando que el cliente jamás pueda elevar privilegios mediante manipulación de datos de entrada.
  • Distinguir con precisión matemática y operativa la identidad verificada de la solicitud (Principal) frente a los identificadores arbitrarios que viajan en el cuerpo JSON o parámetros de ruta.
  • Aplicar el principio de denegación por defecto (Fail-closed / Deny-by-default) en controladores HTTP, servicios de dominio y consultas de persistencia.
  • Evaluar el compromiso arquitectónico (tradeoff) entre revelar HTTP 403 Forbidden versus ocultar la existencia con HTTP 404 Not Found.
  • Discriminar con rigor conceptual los vectores de ataque y mecanismos de defensa de XSS, CSRF y CORS, comprendiendo el rol exacto de los atributos de cookies (HttpOnly, Secure, SameSite).
  • Reconocer y mitigar vulnerabilidades críticas como BOLA/IDOR (Broken Object Level Authorization) y condiciones de carrera de verificación/uso (TOCTOU).

Antes de empezar#

Necesitas tener claros los fundamentos de contratos HTTP (métodos, códigos de estado, encabezados), serialización JSON y transacciones en base de datos vistos en los módulos 02 y 04.

⚠️ Advertencia

Los ejemplos de esta lección ilustran el razonamiento arquitectónico de seguridad y las reglas de autorización en el código de aplicación. No implementes criptografía casera ni algoritmos propios de almacenamiento de contraseñas o emisión de tokens. En un entorno de producción, utiliza siempre implementaciones auditadas y mantenidas (como argon2, bibliotecas estándar de sesiones u OpenID Connect) configuradas bajo HTTPS estricto y un modelo de amenazas formal.

Si encuentras términos desconocidos durante el estudio de esta lección, consulta el glosario técnico.


Diccionario de conceptos de la lección (Glosario desde cero)#

Para construir una mentalidad rigurosa de seguridad defensiva, analizamos cada concepto fundamental bajo cuatro dimensiones: intuición cotidiana, definición técnica formal, ejemplo concreto en código o HTTP y concepción errónea frecuente.

1. Autenticación (AuthN) vs Autorización (AuthZ)#

  • Intuición cotidiana: Al entrar a un edificio corporativo, muestras tu pasaporte o documento de identidad en la recepción: eso es autenticación (demuestra quién eres). Que tu tarjeta de acceso te permita abrir la puerta de la sala de servidores en el piso 4 o solo el comedor general es autorización (qué acciones puedes ejecutar sobre qué áreas).
  • Definición técnica precisa:
  • Autenticación (AuthN): Proceso criptográfico y de verificación de identidad mediante el cual el sistema confirma que una entidad (usuario, servicio, proceso) es verdaderamente quien afirma ser, a través de factores conocidos (contraseña), poseídos (token, llave criptográfica) o inherentes (biometría).
  • Autorización (AuthZ): Proceso de evaluación de políticas de control de acceso que determina si una identidad ya autenticada dispone de los privilegios necesarios para ejecutar una operación (READ, CREATE, UPDATE, DELETE) sobre una entidad o recurso específico.
  • Ejemplo en código / HTTP:
HTTP
  // AuthN exitosa: El token Bearer demuestra que la solicitud proviene del usuario con ID 9
  GET /api/notes/481 HTTP/1.1
  Authorization: Bearer eyJhbGciOi...

  // AuthZ fallida: El usuario 9 existe y está autenticado, pero la nota 481 pertenece al usuario 7
  HTTP/1.1 403 Forbidden
  Content-Type: application/json

  {"error": "FORBIDDEN", "detail": "User 9 is not authorized to access note 481"}
  • Concepción errónea: Creer que porque una ruta cuenta con un middleware que verifica que el usuario "esté logueado", la ruta ya es segura. Si el middleware solo comprueba que exista una sesión válida pero no valida que el usuario sea el dueño del recurso solicitado en la URL, el sistema es vulnerable a ataques de acceso horizontal (BOLA/IDOR).

2. Principal (Identidad verificada en el servidor)#

  • Intuición cotidiana: Un gafete de visitante sellado y plastificado por el oficial de seguridad. No importa que el visitante escriba con bolígrafo "Director General" en su cuaderno personal: los guardias del edificio solo obedecen lo que indica el gafete oficial sellado.
  • Definición técnica precisa: Entidad inmutable construida y validada por el servidor (normalmente a través de un middleware de autenticación) tras verificar una credencial criptográfica o una sesión activa en la base de datos. Representa la identidad fidedigna del solicitante durante el ciclo de vida de la petición HTTP y se inyecta en el contexto de ejecución de la aplicación.
  • Ejemplo en código:
RUST
  #[derive(Debug, Clone, PartialEq, Eq)]
  pub struct AuthenticatedPrincipal {
      pub user_id: UserId,
      pub role: UserRole,
  }
  • Concepción errónea: Confundir la identidad verificada del Principal con identificadores enviados por el cliente en el cuerpo de la petición (como {"userId": 12}). El cliente jamás debe tener la capacidad de definir o alterar su propio Principal.

3. Sesión con estado en servidor (Stateful Session) vs Token firmado (JWT / Bearer)#

  • Intuición cotidiana: Una sesión con estado es como el guardarropa de un teatro: te entregan una ficha plástica con el número 42; tu abrigo y pertenencias permanecen en el almacén del teatro y el empleado busca en sus estantes cada vez que presentas la ficha. Un token firmado (JWT) es como un boleto de avión impreso con sello de agua: contiene tu nombre, asiento y vuelo impresos directamente en el papel; el guardia no necesita llamar a la aerolínea para saber tu asiento, basta con verificar el sello de agua.
  • Definición técnica precisa:
  • *Sesión con estado (Stateful):* El servidor genera un identificador aleatorio criptográficamente seguro (Session ID), lo almacena en una base de datos o almacén en memoria (Redis/PostgreSQL) junto con los datos del usuario, y lo envía al navegador en una cookie. Para invalidar el acceso (cierre de sesión o revocación inmediata), el servidor simplemente borra el registro en su almacén.
  • *Token firmado (Stateless JWT): El servidor empaqueta los datos de identidad en un payload JSON, genera una firma digital utilizando una clave secreta o par asimétrico, y entrega el token al cliente. Cualquier servidor con la clave puede verificar la autenticidad sin consultar una base de datos. Sin embargo, revocar un JWT antes de su expiración requiere mantener listas de bloqueo (denylists*), reintroduciendo estado en el sistema.
  • Ejemplo en código / HTTP:
HTTP
  // Sesión con estado (cookie que solo almacena un puntero opaco):
  Cookie: sid=a8f9c2d1e4b7890a3c2...

  // Token firmado (contiene datos serializados y firma criptográfica):
  Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMiIsImV4cCI6MTczMDAwMDAwMH0.K7j8...
  • Concepción errónea: Pensar que los JWT son inherentemente "mejores y más modernos" que las sesiones con estado. Los JWT trasladan la complejidad al cliente, aumentan el tamaño de cada cabecera HTTP y hacen que la revocación instantánea de sesiones comprometidas sea sumamente compleja sin una base de datos de revocación.

4. Hashing de contraseñas con sal (Argon2id / bcrypt vs SHA-256)#

  • Intuición cotidiana: Una licuadora industrial que tritura una receta secreta junto con un puñado de arena único y diferente para cada persona: es imposible reconstruir la fruta original a partir de la mezcla, y dos personas que licúen la misma fruta obtendrán texturas completamente distintas debido a la arena personalizada.
  • Definición técnica precisa:
  • *Sal criptográfica (Salt): Cadena de bytes generada aleatoriamente mediante un generador criptográfico (CSPRNG) que se concatena a la contraseña antes de aplicar el algoritmo de hash. Garantiza que dos contraseñas idénticas produzcan hashes completamente distintos, neutralizando ataques basados en tablas precalculadas (Rainbow Tables*).
  • Funciones de derivación de claves adaptativas (Argon2id, bcrypt): Algoritmos intencionalmente lentos y costosos en memoria (memory-hard) y CPU, cuyo factor de trabajo (work factor) puede ajustarse a lo largo del tiempo. Contrastan con funciones criptográficas de propósito general como SHA-256, que están diseñadas para ser ultrarrápidas y permiten a un atacante evaluar miles de millones de intentos por segundo mediante tarjetas gráficas (GPU) o hardware dedicado (ASIC).
  • Ejemplo en código (Esquema conceptual de almacenamiento):
TEXT
  // Hash generado con Argon2id (incluye algoritmo, costo de memoria, iteraciones, sal y hash):
  $argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHR2YWx1ZQ$qU7L...
  • Concepción errónea: Creer que aplicar sha256(password + salt) es seguro. SHA-256 fue diseñado para verificar la integridad de gigabytes de archivos a máxima velocidad. Un atacante con una GPU moderna puede calcular más de 5,000 millones de hashes SHA-256 por segundo, haciendo trivial la rotura por fuerza bruta de contraseñas de longitud media.

5. Denegación por defecto (Fail-closed / Deny-by-default)#

  • Intuición cotidiana: La puerta de una bóveda bancaria con cerradura electromagnética: si se corta el suministro eléctrico o el sistema de control se congela, los pernos mecánicos caen y la puerta permanece herméticamente bloqueada, impidiendo el acceso a cualquier persona hasta que el sistema se restaure de manera controlada.
  • Definición técnica precisa: Principio de diseño de seguridad donde el estado base de cualquier petición es acceso denegado. El acceso solo se otorga si se cumple una regla de autorización explícita y afirmativa. Ante cualquier fallo de infraestructura, consulta no resuelta, tiempo de espera agotado (timeout) o condición inesperada, el sistema aborta la operación y deniega el permiso (fail-closed), evitando conceder acceso por accidente (fail-open).
  • Ejemplo en código:
RUST
  pub fn check_permission(policy_result: Result<bool, DbError>) -> AccessDecision {
      match policy_result {
          Ok(true) => AccessDecision::Allow,
          Ok(false) => AccessDecision::Deny,
          // Ante cualquier error de red o timeout, denegar inmediatamente:
          Err(_) => AccessDecision::Deny,
      }
  }
  • Concepción errónea: Escribir bloques de control donde el caso por defecto o de error sea permisivo, como if has_explicit_block() { return Deny; } else { return Allow; }. Si la consulta que comprueba bloqueos falla, el atacante obtiene acceso ilimitado.

6. Vulnerabilidad BOLA / IDOR (Broken Object Level Authorization / Insecure Direct Object References)#

  • Intuición cotidiana: Un casillero de gimnasio cuya llave abre únicamente el casillero 42, pero tú descubres que con solo cambiar la etiqueta del número en la cerradura a 43, la cerradura se abre y te permite tomar las pertenencias del vecino.
  • Definición técnica precisa: Vulnerabilidad de seguridad clasificada como la amenaza número 1 en el OWASP API Security Top 10. Ocurre cuando un endpoint expone un identificador de objeto en la URL o cuerpo de la petición (por ejemplo /api/notes/481), y el servidor confía ciegamente en que el solicitante tiene permiso para interactuar con dicho identificador sin comprobar la relación de propiedad o membresía en la base de datos.
  • Ejemplo en HTTP:
HTTP
  // Atacante con UserId=9 intenta modificar la nota del UserId=7:
  PATCH /api/notes/481 HTTP/1.1
  Host: api.example.com
  Cookie: sid=attacker_valid_session

  {"title": "Compromised Title"}

  // Si el servidor solo verifica que sid sea válido pero no que owner_id == 9:
  // ¡Vulnerabilidad BOLA materializada!
  • Concepción errónea: Pensar que con utilizar UUIDs v4 en lugar de enteros autoincrementales se elimina la vulnerabilidad BOLA. Los UUIDs mitigan la enumeración secuencial ciega, pero si un atacante obtiene un UUID por fuga de información, enlaces compartidos o registros de red, la ausencia de control de autorización por objeto sigue permitiendo la intrusión.

7. Atributos de cookies de seguridad (HttpOnly, Secure, SameSite=Lax/Strict)#

  • Intuición cotidiana: Un sobre confidencial que indica: "Entregar únicamente en mano del destinatario bajo techo cubierto (Secure), no permitir que terceros lean su contenido bajo ninguna circunstancia (HttpOnly), y no entregar si el mensajero proviene de otra empresa (SameSite)".
  • Definición técnica precisa:
  • HttpOnly: Directiva que instruye al navegador prohibir el acceso a la cookie mediante interfaces de scripting (document.cookie en JavaScript), mitigando la exfiltración de credenciales en caso de ataques XSS.
  • Secure: Directiva que restringe el envío de la cookie exclusivamente a través de canales cifrados con TLS (https://), impidiendo su transmisión en texto plano ante ataques de intermediario (Man-in-the-Middle).
  • SameSite: Control que regula si la cookie debe adjuntarse en solicitudes iniciadas desde sitios web cruzados (Cross-Site). Strict impide el envío en cualquier solicitud originada en un sitio externo; Lax permite el envío en navegaciones de nivel superior seguras (como clics en enlaces <a> con método GET), pero bloquea el envío en peticiones mutables (POST, PUT, formularios cruzados), protegiendo contra CSRF común.
  • Ejemplo en HTTP:
HTTP
  Set-Cookie: sid=e4b7890a3c2...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=3600
  • Concepción errónea: Asumir que HttpOnly impide ataques XSS. HttpOnly impide que el script malicioso lea el valor de la cookie, pero el script ejecutándose en el contexto del navegador de la víctima todavía puede realizar peticiones HTTP legítimas (fetch('/api/transfer')) que el navegador autenticará adjuntando la cookie automáticamente.

8. Fijación de sesión (Session Fixation) y rotación de ID#

  • Intuición cotidiana: Un ladrón que deja un candado abierto de su propiedad en un casillero vacío del gimnasio. Cuando un usuario desprevenido llega, guarda sus cosas y cierra el candado con llave creyendo que el casillero se lo asignó el gimnasio, el ladrón utiliza su copia de la llave para abrir el casillero y robar el contenido.
  • Definición técnica precisa: Vector de ataque en el cual el atacante obtiene o establece un identificador de sesión conocido en el navegador de la víctima antes de que esta inicie sesión. Si la aplicación web conserva el mismo identificador de sesión tras una autenticación exitosa, el atacante utiliza su identificador previo para secuestrar la sesión recién autenticada. La mitigación estándar es la rotación obligatoria del identificador de sesión: en el instante exacto en que un usuario se autentica con éxito, el servidor destruye el identificador anterior y emite uno completamente nuevo y aleatorio.
  • Ejemplo en HTTP:
HTTP
  // 1. Sesión anónima previa:
  Cookie: sid=anonymous_session_123

  // 2. Tras POST /api/login exitoso, el servidor DEBE rotar el identificador:
  HTTP/1.1 200 OK
  Set-Cookie: sid=authenticated_session_987_NEW; Path=/; Secure; HttpOnly; SameSite=Lax
  • Concepción errónea: Creer que con invalidar la sesión únicamente al hacer "Cerrar sesión" (Logout) es suficiente. La rotación debe ocurrir imperativamente en el momento del Login para cortar cualquier enlace con identificadores asignados durante la fase anónima.

9. Diferencia exacta entre XSS, CSRF y CORS#

  • Intuición cotidiana:
  • XSS: Un impostor se disfraza de empleado dentro de la oficina de tu banco y da órdenes directamente en tu nombre desde adentro del edificio.
  • CSRF: Un timador te engaña en la calle para que envíes una carta preimpresa con tu firma auténtica a tu banco pidiéndoles que transfieran dinero a su cuenta.
  • CORS: Un protocolo de cortesía entre embajadas que define qué cartas provenientes de países extranjeros tienen permitido entrar a leer archivos de la biblioteca central.
  • Definición técnica precisa:
  • *XSS (Cross-Site Scripting):* Inyección y ejecución de código script arbitrario (JavaScript) en el contexto de seguridad del origen de la víctima, debido a la falta de sanitización o codificación de datos no confiables renderizados en el DOM.
  • *CSRF (Cross-Site Request Forgery):* Explotación de la confianza que una aplicación web tiene en el navegador de un usuario autenticado, forzando al navegador a emitir peticiones no deseadas hacia un origen vulnerable que adjunta automáticamente las credenciales de sesión (cookies).
  • *CORS (Cross-Origin Resource Sharing): Mecanismo de seguridad implementado por los navegadores que relaja de forma controlada la Política del Mismo Origen (Same-Origin Policy*), permitiendo a scripts que corren en un origen (ej. app.frontend.com) leer respuestas emitidas por otro origen (ej. api.backend.com) basándose en cabeceras HTTP negociadas.
  • Concepción errónea: Pensar que configurar CORS de manera estricta protege a una API contra ataques CSRF o ataques con herramientas como curl. CORS es una restricción implementada exclusivamente por el navegador para scripts; no impide que una petición mutante llegue al servidor ni protege contra clientes de consola.

Modelo mental y análisis en profundidad#

DIAGRAMA MERMAID
sequenceDiagram
  participant B as Browser
  participant A as Application
  participant D as Data store
  B->>A: Request with session credential
  A->>A: Verify session and build principal
  A->>D: Load resource and ownership rule
  D-->>A: Resource plus policy facts
  A->>A: Authorize principal for action
  A-->>B: Allowed result or denied response

Diagrama estático del tema
Diagrama estático del tema
Fuente editable: Mermaid.

Desglose paso a paso 1: La falacia de la seguridad en el cliente (UI vs API)#

En el desarrollo de interfaces modernas con React, Vue o HTML/CSS, es una práctica habitual condicionar la visibilidad de elementos visuales:

TSX
// Client-side UI ergonomics (NOT security!)
function NoteActions({ note, currentUser }: Props) {
  if (currentUser.id !== note.ownerId) {
    return null; // The "Delete" button is hidden from the UI
  }
  return <button onClick={() => deleteNote(note.id)}>Delete Note</button>;
}

Ocultar este botón mejora la experiencia de usuario (UX) para evitar frustración visual, pero no constituye ninguna barrera de seguridad. Un atacante nunca interactúa exclusivamente a través de los botones renderizados en el DOM:

  1. El atacante abre las herramientas de desarrollo del navegador (DevTools) o una terminal con curl o un script en Python.
  2. Construye una solicitud HTTP pura dirigida directamente al endpoint de la API: ``bash curl -X DELETE https://api.example.com/notes/481 \ -H "Cookie: sid=attacker_valid_session" ``
  3. Alternativamente, en un endpoint de actualización (PATCH /notes/481), el atacante puede inyectar en el cuerpo JSON campos forzados: ``json { "userId": 7, "title": "Malicious takeover" } ``

Si el controlador en el backend extrae el ID de usuario desde el cuerpo de la petición (body.userId) o simplemente asume que si la petición llegó es porque el usuario tenía visible el botón en la interfaz, el sistema sufrirá una vulnerabilidad catastrófica de BOLA/IDOR.

🛡️ Información Crítica

Regla inviolable de frontera: La red es hostil y el cliente es completamente manipulable por el usuario. El servidor debe asumir que todo byte recibido en la cabecera, URL o cuerpo es potencialmente malicioso. La identidad del solicitante debe provenir exclusivamente del contexto de sesión verificado por el servidor, jamás de campos autoproclamados por el cliente.


Desglose paso a paso 2: Condiciones de carrera en autorización (TOCTOU)#

Una falla recurrente al diseñar autorización por recurso es separar la verificación del permiso de la ejecución de la mutación en dos pasos secuenciales no coordinados:

RUST
// TOCTOU Anti-pattern: Time-of-Check to Time-of-Use
pub fn update_note_naive(db: &Database, principal: &Principal, note_id: u64, new_title: &str) -> Result<(), Error> {
    // Paso 1: Verificación de propiedad (Check)
    let owner_id = db.query_owner("SELECT owner_id FROM notes WHERE id = $1", note_id)?;
    if owner_id != principal.user_id {
        return Err(Error::Forbidden);
    }

    // <-- VENTANA DE CARRERA (Race Condition):
    // Justo aquí, otra transacción concurrente transfiere la nota a otro dueño,
    // o el usuario pierde los permisos de edición sobre la entidad.

    // Paso 2: Mutación (Use)
    db.execute("UPDATE notes SET title = $1 WHERE id = $2", new_title, note_id)?;
    Ok(())
}

Este fenómeno se denomina TOCTOU (Time-Of-Check to Time-Of-Use). Entre el instante en que el sistema valida la propiedad de la fila y el instante en que ejecuta la mutación persistente, el estado del recurso puede haber cambiado concurrentemente en la base de datos.

La solución atómica: Fusión del predicado de autorización en la consulta#

En sistemas relacionales como PostgreSQL, la forma más elegante, eficiente y segura de eliminar las condiciones de carrera TOCTOU consiste en incorporar el predicado de autorización directamente en la cláusula WHERE de la mutación SQL:

SQL
-- Operación atómica de autorización y mutación bajo un único lock de fila:
UPDATE notes
SET title = $1, updated_at = NOW()
WHERE id = $2 AND owner_id = $3;

¿Qué ocurre en el motor de base de datos?

  1. El motor adquiere un bloqueo exclusivo a nivel de fila (ExclusiveLock en la tupla).
  2. Evalúa atómicamente si existe una fila cuyo id coincida con $2 Y cuyo owner_id coincida con el $3 verificado del Principal.
  3. Si la fila no existe o si pertenece a otro dueño, la consulta afecta exactamente 0 filas (rows_affected == 0).
  4. Si afecta 0 filas, el servicio mapea este resultado a un error de autorización o existencia, garantizando que ninguna mutación ilegítima persista jamás.

Desglose paso a paso 3: El compromiso de revelación: ¿403 Forbidden o 404 Not Found?#

Cuando un principal autenticado (ej. usuario 9) intenta interactuar con un recurso que pertenece a otro usuario (ej. usuario 7), la arquitectura debe elegir qué código de estado HTTP retornar:

Código HTTPSignificado SemánticoVentaja ArquitectónicaRiesgo / Costo Operativo
403 Forbidden"Sé quién eres y el recurso existe, pero tus permisos actuales no te permiten acceder a él".Depurabilidad máxima: Facilita el diagnóstico de errores para clientes legítimos, logs de auditoría y equipos de soporte. Comunica claramente que la autenticación fue exitosa pero falló la política.*Enumeración de recursos (Resource Harvesting):* Un atacante que pruebe identificadores secuenciales (/notes/1, /notes/2, /notes/3) sabrá exactamente qué notas existen en el sistema al observar cuáles devuelven 403 frente a las que devuelven 404.
404 Not Found"No se encontró ningún recurso bajo este identificador para tu contexto de acceso".Confidencialidad absoluta: Previene la enumeración de recursos privados. El atacante no puede distinguir entre un ID que no existe y un ID privado de otro usuario.Complejidad de depuración: Si un usuario con credenciales degradadas o error de configuración intenta abrir un recurso propio legítimo, verá un 404 y asumirá que los datos fueron eliminados, dificultando el soporte técnico.

Recomendación de diseño#

Para recursos altamente sensibles o privados (como notas personales, historiales médicos o claves privadas), la aplicación debe adoptar 404 Not Found para cualquier usuario que no sea el propietario, tratando el recurso como si no existiera en el universo accesible de ese usuario. Para recursos públicos con permisos jerárquicos (como un documento de empresa visible donde solo los administradores pueden editar), retornar 403 Forbidden en la operación de escritura es la semántica correcta.


Desglose paso a paso 4: Matriz comparativa: XSS vs CSRF vs CORS#

La confusión entre estas tres tecnologías es un error recurrente en entrevistas y auditorías de seguridad. Analicemos sus diferencias estructurales:

DimensiónXSS (Cross-Site Scripting)CSRF (Cross-Site Request Forgery)CORS (Cross-Origin Resource Sharing)
Vector de ataque principalInyección de código malicioso (HTML/JS) en el navegador de la víctima a través de entradas no sanitizadas.Un sitio web malicioso de terceros induce al navegador de la víctima a emitir una petición HTTP hacia un sitio de destino donde está autenticado.No es un ataque; es una medida de relajación controlada de la Política del Mismo Origen (Same-Origin Policy).
Mecanismo del navegador abusadoCapacidad del motor de renderizado del navegador para ejecutar cualquier script inyectado en el DOM con los privilegios del origen actual.El comportamiento estándar del navegador de adjuntar automáticamente cookies y credenciales ambientales en peticiones hacia el dominio destino.Intentos de scripts en un origen (attacker.com) de leer respuestas de otro origen (bank.com) sin permiso explícito de cabeceras.
¿Quién ejecuta la acción?El código malicioso del atacante ejecutándose dentro del origen de la víctima.El navegador de la víctima emitiendo una petición legítima dirigida hacia la API de destino.El navegador bloqueando o permitiendo la lectura de respuestas según cabeceras Access-Control-Allow-Origin.
Impacto de la brechaRobo de tokens en localStorage, captura de teclas pulsadas (keylogging), manipulación del DOM, redirección de pagos.Ejecución de acciones no autorizadas en nombre de la víctima (cambio de correo, transferencias, borrado de datos).Mala configuración (Access-Control-Allow-Origin: * con credenciales) permite a sitios externos exfiltrar respuestas privadas.
Mecanismo real de mitigación- Codificación contextual de salida (Context-aware escaping).
- Políticas de Seguridad de Contenido estricta (CSP).
- Atributo HttpOnly en cookies de sesión.
- Atributo SameSite=Lax o SameSite=Strict en cookies.
- Tokens anti-CSRF criptográficos validados por el servidor.
- Validación de cabeceras de origen (Sec-Fetch-Site, Origin).
- Configurar listas blancas estrictas de orígenes permitidos.
- Nunca usar comodines (*) combinados con Access-Control-Allow-Credentials: true.

Ejemplo razonado de arquitectura segura#

A continuación se presenta un diseño completo en Rust estándar (std-only) que modela el ciclo de vida de una solicitud protegida:

  1. El contexto de la petición contiene un AuthenticatedPrincipal inmutable inyectado por el sistema de autenticación.
  2. La carga útil (payload) no confiable contiene datos arbitrarios que son ignorados por el motor de permisos.
  3. El servicio de notas aplica autorización estricta a nivel de objeto mediante consulta atómica y principio de denegación por defecto.
RUST
// Compilable and verified with rustc --test
use std::collections::HashMap;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(pub u64);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NoteId(pub u64);

/// Represents a verified identity constructed exclusively by trusted auth middleware.
/// The client has ZERO influence over the contents of this struct.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthenticatedPrincipal {
    pub user_id: UserId,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Note {
    pub id: NoteId,
    pub owner_id: UserId,
    pub title: String,
}

/// Untrusted client payload. Notice that any incoming `untrusted_user_id` is untrusted data!
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchNotePayload {
    pub title: String,
    pub untrusted_user_id: Option<UserId>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthzError {
    NotFound,
    Forbidden,
}

pub struct NoteRepository {
    storage: HashMap<NoteId, Note>,
}

impl NoteRepository {
    pub fn new() -> Self {
        Self {
            storage: HashMap::new(),
        }
    }

    pub fn seed(&mut self, note: Note) {
        self.storage.insert(note.id, note);
    }

    /// Read operation: returns NotFound if note does not exist,
    /// or Forbidden if caller is not the verified owner.
    pub fn get_note_secure(
        &self,
        principal: &AuthenticatedPrincipal,
        note_id: NoteId,
    ) -> Result<&Note, AuthzError> {
        let note = self.storage.get(&note_id).ok_or(AuthzError::NotFound)?;
        if note.owner_id != principal.user_id {
            return Err(AuthzError::Forbidden);
        }
        Ok(note)
    }

    /// Atomic mutation simulation of:
    /// UPDATE notes SET title = $1 WHERE id = $2 AND owner_id = $3
    pub fn update_title_atomic(
        &mut self,
        principal: &AuthenticatedPrincipal,
        note_id: NoteId,
        new_title: &str,
    ) -> Result<&Note, AuthzError> {
        let note = self.storage.get_mut(&note_id).ok_or(AuthzError::NotFound)?;
        // Fail-closed ownership check
        if note.owner_id != principal.user_id {
            return Err(AuthzError::Forbidden);
        }
        note.title = new_title.to_string();
        Ok(note)
    }
}

Práctica independiente#

Escenario de laboratorio#

Dispones de una base de datos con una nota protegida:

  • NoteId: 481
  • owner_id: UserId(7)
  • title: "Confidential Field Note"

Llega una solicitud HTTP de un atacante autenticado:

  • Principal: UserId(9)
  • Operaciones a evaluar:
  1. GET /api/notes/481
  2. PATCH /api/notes/481 con payload {"title": "Compromised Title", "userId": 7}
  3. GET /api/notes/999 (identificador que no existe en el sistema)

Disciplina TDD: RED arrow GREEN arrow REFACTOR#

  1. Fase RED: Escribe una prueba de caracterización de seguridad donde el manejador tome ingenuamente el userId presente en el cuerpo JSON para decidir si autoriza la mutación. Observa cómo el atacante (usuario 9) logra sobrescribir la nota del usuario 7 enviando "userId": 7.
  2. Fase GREEN: Modifica el caso de uso para exigir la firma update_title_atomic(principal: &AuthenticatedPrincipal, ...) donde el predicado de autorización utilice únicamente principal.user_id. Comprueba que la prueba rechace al atacante con Err(AuthzError::Forbidden).
  3. Fase REFACTOR: Separa la responsabilidad de autenticar la sesión (middleware) de la política de autorización por recurso (dominio / repositorio). Asegura que si la búsqueda del recurso o la evaluación de la política falla, el resultado sea denegación inmediata (fail-closed).

Arnés de pruebas automatizado verificable (rustc --test)#

Puedes verificar este código ejecutando directamente en tu terminal:

BASH
rustc --test lesson16_auth_test.rs
./lesson16_auth_test
RUST
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn owner_can_read_and_update_their_own_note() {
        let mut repo = NoteRepository::new();
        repo.seed(Note {
            id: NoteId(481),
            owner_id: UserId(7),
            title: "Confidential Field Note".to_string(),
        });

        let legitimate_owner = AuthenticatedPrincipal { user_id: UserId(7) };

        // 1. Read succeeds
        let note = repo.get_note_secure(&legitimate_owner, NoteId(481)).unwrap();
        assert_eq!(note.title, "Confidential Field Note");

        // 2. Mutation succeeds
        let updated = repo
            .update_title_atomic(&legitimate_owner, NoteId(481), "Legitimate Edit")
            .unwrap();
        assert_eq!(updated.title, "Legitimate Edit");
    }

    #[test]
    fn non_owner_is_forbidden_even_when_forging_user_id_in_body() {
        let mut repo = NoteRepository::new();
        repo.seed(Note {
            id: NoteId(481),
            owner_id: UserId(7),
            title: "Confidential Field Note".to_string(),
        });

        let attacker_principal = AuthenticatedPrincipal { user_id: UserId(9) };
        let forged_payload = PatchNotePayload {
            title: "Compromised Title".to_string(),
            untrusted_user_id: Some(UserId(7)), // Forged ownership claim!
        };

        // 1. Attempt to read: MUST return Forbidden
        let read_result = repo.get_note_secure(&attacker_principal, NoteId(481));
        assert_eq!(read_result, Err(AuthzError::Forbidden));

        // 2. Attempt to patch: payload.untrusted_user_id is ignored, MUST return Forbidden
        let patch_result = repo.update_title_atomic(
            &attacker_principal,
            NoteId(481),
            &forged_payload.title,
        );
        assert_eq!(patch_result, Err(AuthzError::Forbidden));

        // 3. Verify state in repository remained intact
        let legitimate_owner = AuthenticatedPrincipal { user_id: UserId(7) };
        let note_in_store = repo.get_note_secure(&legitimate_owner, NoteId(481)).unwrap();
        assert_eq!(note_in_store.title, "Confidential Field Note");
    }

    #[test]
    fn missing_resource_returns_not_found() {
        let repo = NoteRepository::new();
        let principal = AuthenticatedPrincipal { user_id: UserId(7) };
        assert_eq!(repo.get_note_secure(&principal, NoteId(999)), Err(AuthzError::NotFound));
    }
}

Code smell: Confundir identidad declarada con identidad autenticada#

El síntoma en el código#

RUST
// DELIBERATELY INSECURE CODE SMELL
pub async fn handle_patch_note(
    req_body: web::Json<PatchNoteRequest>, // Contains { "user_id": 7, "title": "New" }
    path_id: web::Path<u64>,
    db: web::Data<DatabasePool>,
) -> HttpResponse {
    let note = db.find_note(*path_id).await.unwrap();

    // VULNERABILITY: Comparing database owner with untrusted payload field!
    if req_body.user_id == note.owner_id {
        db.update_title(*path_id, &req_body.title).await.unwrap();
        HttpResponse::Ok().json(note)
    } else {
        HttpResponse::Forbidden().finish()
    }
}

Diagnóstico de ingeniería#

  • Evidencia empírica: El parámetro req_body.user_id nace de la deserialización de bytes arbitrarios transmitidos a través del socket TCP por el cliente.
  • Hipótesis del fallo: El autor del código asume que el frontend es el único intermediario que envía solicitudes y que la interfaz solo envía el ID del usuario activo.
  • Consecuencia destructiva: Cualquier usuario con una sesión válida (o incluso anónima si no se valida antes) puede suplantar la identidad de cualquier otro usuario simplemente alterando un número en el cuerpo JSON de su petición.
  • Corrección arquitectónica mínima:
  1. Extraer el identificador del usuario únicamente desde la extensión de solicitud req.extensions().get::<AuthenticatedPrincipal>() proporcionada por el middleware de autenticación.
  2. Eliminar completamente el campo user_id del struct PatchNoteRequest.

Preguntas de comprensión profunda y diagnóstico socrático#

Pregunta 1: La falacia de la seguridad del lado del cliente (UI vs API)#

  • Escenario: Un equipo de desarrollo oculta el botón "Eliminar proyecto" en una interfaz React evaluando currentUser.role !== 'Admin'. Sin embargo, en el controlador de la API REST DELETE /api/projects/:id, el desarrollador omitió la verificación de rol asumiendo que "nadie puede hacer clic en un botón que no está en pantalla".
  • Inciso A: Explica con exactitud cómo un atacante con cuenta estándar ejecuta la eliminación del proyecto sin tocar la interfaz web.
  • Inciso B: ¿Qué vulnerabilidad del OWASP API Security Top 10 se acaba de manifestar y cuál es la regla de frontera violada?
  • Guía de razonamiento: Rastrea el recorrido físico del paquete TCP/HTTP. Demuestra por qué el servidor procesa flujos de bytes y carece de conocimiento sobre si un botón existe o fue renderizado en el navegador de la víctima.

Pregunta 2: Condiciones de carrera TOCTOU en autorización#

  • Escenario: Para actualizar una entrada sensible, un servicio ejecuta primero:

SELECT owner_id, team_id FROM documents WHERE id = $1 Comprueba en memoria if principal.team_id == doc.team_id y posteriormente ejecuta: UPDATE documents SET body = $1 WHERE id = $2

  • Inciso A: En un sistema de alta concurrencia, ¿qué intercalado de eventos (interleaving) permite que un usuario expulsado del equipo milisegundos antes logre persistir cambios en el documento?
  • Inciso B: Escribe la consulta SQL atómica que garantiza la invariante de autorización a nivel de base de datos sin requerir bloqueos manuales complejos.
  • Guía de razonamiento: Analiza la ventana temporal entre la lectura y la escritura. Contrasta cómo los niveles de aislamiento y los bloqueos de fila resuelven la ventana de vulnerabilidad.

Pregunta 3: Compromiso de divulgación: 403 Forbidden vs 404 Not Found#

  • Escenario: Una plataforma de salud mental almacena notas de terapia en /api/therapy-sessions/:id. Si un usuario intenta consultar la sesión 502 que pertenece a otro paciente:
  • Inciso A: ¿Qué información sensible filtra el sistema si responde con HTTP 403 Forbidden frente a un atacante que realiza un ataque de fuerza bruta sobre los IDs del 1 al 1000?
  • Inciso B: ¿Qué problema operativo surge para el equipo de soporte si la API responde con HTTP 404 Not Found cuando un paciente legítimo experimenta un fallo temporal de sincronización de permisos?
  • Guía de razonamiento: Pesa el balance entre confidencialidad (evitar la confirmación de existencia de recursos privados) y observabilidad/depurabilidad en producción.

Pregunta 4: La anatomía de CSRF y el alcance real de HttpOnly#

  • Escenario: Un desarrollador afirma: "Nuestra aplicación almacena la cookie de sesión con el atributo HttpOnly. Por lo tanto, es imposible que suframos ataques de tipo CSRF y no necesitamos implementar tokens anti-CSRF ni SameSite".
  • Inciso A: ¿Por qué la afirmación del desarrollador es conceptualmente errónea? ¿Qué hace el navegador de manera automática cuando un sitio malicioso incluye una etiqueta <form action="https://bank.com/transfer" method="POST">?
  • Inciso B: Si la cookie tuviera HttpOnly pero careciera de SameSite, ¿puede el sitio del atacante leer el contenido de la respuesta de la transferencia? ¿Puede forzar la ejecución de la transferencia? Explica la diferencia.
  • Guía de razonamiento: Recuerda que HttpOnly restringe el acceso al valor de la cookie mediante JavaScript en el DOM, pero no altera la regla histórica del navegador de adjuntar cookies automáticamente a peticiones de red salientes.

Pregunta 5: Funciones de hash criptográfico vs Derivación de claves adaptativas#

  • Escenario: Un sistema almacena las contraseñas calculando SHA-256(password + salt). El equipo argumenta que al usar una sal aleatoria de 32 bytes generada con CSPRNG, las contraseñas son completamente seguras.
  • Inciso A: ¿Por qué SHA-256 es una pésima elección para proteger contraseñas frente a un atacante que descarga un volcado de la base de datos?
  • Inciso B: ¿Qué propiedades matemáticas y físicas hacen que algoritmos como Argon2id o bcrypt sean resistentes a ataques mediante clústeres de GPUs o circuitos integrados de aplicación específica (ASIC)?
  • Guía de razonamiento: Compara el costo computacional y de memoria de un algoritmo diseñado para rendimiento de flujo (SHA-256) frente a una función con costo configurable de iteraciones y memoria dura (memory-hardness).

Criterio de salida#

Puedes considerar superada esta lección cuando:

  • Separas tajantemente la fase de autenticación (verificación de credenciales y creación del Principal) de la autorización por recurso (validación de propiedad o permisos sobre la entidad solicitada).
  • Escribes consultas de mutación SQL que combinan el identificador del recurso con el identificador del propietario (WHERE id = $1 AND owner_id = $2), eliminando condiciones de carrera TOCTOU.
  • Explicas con fluidez técnica por qué ocultar elementos en la interfaz de usuario no ofrece seguridad y cómo una API mitiga vulnerabilidades BOLA/IDOR.
  • Distingues sin ambigüedades los vectores y mitigaciones de XSS, CSRF y CORS, y configuras adecuadamente los atributos de cookies (Secure, HttpOnly, SameSite).
  • Todas las pruebas automatizadas de seguridad compilan y pasan en verde con rustc --test.

Lecturas oficiales#


Siguiente paso#

Continúa con 17. Entrega, operación y observabilidad para aprender a monitorear la salud de tus procesos en producción mediante telemetría correlacionada y percentiles de latencia. Si deseas contrastar tus soluciones y respuestas a las preguntas socráticas, consulta la guía de respuestas de operaciones.

Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)