← Volver al blog
🪣 AWS Security10 min de lectura·

SPA en S3 + CloudFront: la vulnerabilidad de exposición de token y cómo la resuelve AWS

Durante años, servir tu SPA como archivos estáticos en S3 detrás de CloudFront fue —y sigue siendo— una recomendación popular: es barato, escala solo y no pagas cómputo. Pero esa arquitectura arrastra un problema de seguridad que mucha gente no ve: al no haber un backend propio, el token de autenticación, la URL de tu proveedor de identidad y la de tu API terminan expuestos en el navegador. Veamos por qué pasa y qué recomienda AWS para arreglarlo.

Por qué llegamos aquí

La lógica era impecable en su momento: si tu frontend es una SPA (React, Angular, Vue), el navegador solo necesita descargar HTML, CSS y JavaScript. ¿Para qué pagar un servidor encendido las 24 horas? Subes el build a un bucket S3, pones CloudFront delante para caché y HTTPS global, y listo: hosting casi gratis, sin servidores que mantener.

El efecto secundario: ya no tienes ningún componente de servidor propioen la ruta del usuario. Y como la autenticación hay que hacerla en algún lado, se hace donde sí hay código corriendo: el navegador. Ahí empieza el problema.

💡 No es que S3+CloudFront sea inseguro por sí mismo

S3 y CloudFront hacen bien su trabajo (entregar estáticos). El problema es la consecuencia arquitectónica: sin backend propio, la SPA se convierte en un cliente OAuth «público» que maneja tokens en el navegador.

Las tres exposiciones

Cuando la SPA es la que autentica (típicamente con Cognito Hosted UI o Amplify como public client), ocurren tres exposiciones simultáneas:

S3 (SPA)CloudFrontassetsNavegador (SPA)JSaccess_token + refresh_tokenen localStorageconoce URL del IdP y del backenddescarga JS☠️XSS lee token + descubre endpointsCognito / IdPAPI backendlogin directoBearer token directo
SPA estática en S3+CloudFront: el token y los endpoints internos viven en el navegador.
  • 1. Exposición del token: el access_token y el refresh_token se guardan en localStorage/memoria. Un XSS —en tu código o en una dependencia npm comprometida— los lee y los exfiltra. Con el refresh_token, el atacante mantiene acceso prolongado.
  • 2. Exposición de la URL del proveedor de identidad: el navegador habla directo con Cognito/tu IdP, así que su dominio, client_id, scopes y parámetros están a la vista en las DevTools.
  • 3. Exposición de la URL del backend: la SPA llama directamente a tu API, revelando su dominio, rutas y contratos. Un atacante mapea tu infraestructura solo mirando el tráfico de red.

🚫 Y no, el WAF no lo arregla

AWS WAF en CloudFront inspecciona el tráfico entrante a tu distribución (inyecciones, rate limiting, geo-blocking). Pero el robo del token ocurre dentro del navegador y se exfiltra al servidor del atacante, tráfico que nunca pasa por tu CloudFront. El WAF reduce la probabilidad de algunos XSS reflejados; no elimina la clase «token robable desde el navegador».

La solución: reintroducir una capa server-side

El patrón de fondo es el Backend For Frontend: que un componente de servidor —no el navegador— sea el cliente OAuth confidencial, guarde los tokens y exponga al browser solo una cookie de sesión HttpOnly. La buena noticia: AWS te da dos formas gestionadas de hacerlo sin renunciar del todo a la comodidad del hosting estático.

Opción A — ALB con authenticate-oidc

Si tu tráfico puede pasar por un Application Load Balancer, el propio ALB hace la autenticación OIDC por ti. Es un BFF gestionado sin escribir código de tokens.

Navegadorsolo AWSELBAuthSessionCookie (HttpOnly)CloudFrontALBauthenticate-oidcALBtoken OIDCse queda en el ALBCognito / IdPApp / EKScookieOIDC (server-side)x-amzn-oidc-* headers
ALB authenticate-oidc: el token OIDC se queda en el ALB; el navegador solo recibe una cookie.
  • Configuras una regla de listener authenticate-oidc (o authenticate-cognito). El ALB ejecuta el flujo Authorization Code contra Cognito/tu IdP.
  • El ALB guarda los tokens y entrega al navegador la cookie AWSELBAuthSessionCookie (HttpOnly). El token no llega al browser.
  • A tu aplicación le inyecta la identidad vía headers firmados: x-amzn-oidc-data (claims como JWT), x-amzn-oidc-accesstoken y x-amzn-oidc-identity. Tu backend solo valida esos headers.

⚠️ Cuándo NO encaja

El ALB necesita un origen dinámico detrás (app en ECS/EKS/EC2). Si tu SPA es 100% estática servida solo por S3+CloudFront sin nada dinámico, esta opción implica añadir esa capa. Para el caso puramente estático, mira la opción B.

Opción B — CloudFront + Lambda@Edge (Authorization@Edge)

Este es el patrón que AWS documenta específicamente para proteger contenido estático en S3+CloudFront. Una función Lambda@Edge se ejecuta en el borde y actúa como BFF: maneja el flujo OIDC con Cognito, valida la sesión en cada request y transfiere el JWT mediante cookies HttpOnly, de forma transparente para la SPA.

Navegadorsolo cookie HttpOnly (JWT server-side)CloudFront + Lambda@EdgeCloudFrontLambda@Edgevalida sesión + tokenen el borde (server-side)S3 (SPA)CognitoAPI backendcookieassets (si autorizado)OIDCAPI (server-side)
Authorization@Edge: Lambda@Edge maneja el token en el borde; el navegador solo lleva una cookie HttpOnly.
  • La Lambda@Edge intercepta las requests en CloudFront (viewer/origin request), verifica la cookie de sesión y, si falta o expiró, redirige al login de Cognito.
  • Los JWT se transfieren en cookies HttpOnly, no en headers que la SPA tenga que manejar. Así el JavaScript del navegador nunca toca el token.
  • Los assets de S3 solo se sirven a usuarios autenticados, y la URL del IdP/backend queda detrás del edge.

No lo escribas desde cero

AWS mantiene la solución de referencia aws-samples/cloudfront-authorization-at-edge, desplegable como aplicación del Serverless Application Repository. Te da el flujo Cognito + cookies HttpOnly + rotación de tokens ya resuelto.

El rol real de Cognito

Aquí el matiz que confunde a mucha gente: Cognito no resuelve por sí solo la exposición del token. Cognito es el proveedor de identidad; lo que decide tu seguridad es dónde pones el cliente confidencial.

  • Cognito directo desde la SPA (Hosted UI/Amplify como public client) → el token vuelve al navegador → misma vulnerabilidad.
  • Cognito detrás de un ALB o Lambda@Edge → el token se queda server-side → problema resuelto.

Resumen de opciones

Postura correcta: defensa en profundidad, no «una sola bala».

MecanismoQué hace con el token¿Quita el token del navegador?
AWS WAFFiltra tráfico entranteNo
CSP estrictaDificulta ejecutar/exfiltrar XSSParcial
ALB authenticate-oidcToken en el ALB, cookie al browser
CloudFront + Lambda@EdgeToken en el borde, cookie HttpOnly
Cognito (public client en SPA)Token al navegadorNo

En resumen

Servir tu SPA desde S3+CloudFront no es un error, pero deja un hueco: sin backend propio, el token y tus endpoints internos acaban en el navegador. El WAF ayuda con el perímetro, pero no cierra esa clase de ataque. La solución que recomienda AWS es reintroducir una capa server-side que maneje el token —ALB con authenticate-oidc si tienes origen dinámico, o Lambda@Edge (Authorization@Edge) para hosting puramente estático— dejando al navegador solo con una cookie HttpOnly. Cognito es tu IdP; la seguridad la define dónde vive el cliente confidencial.