OAuth 2.0 Scope Creep: el vector de ataque que el incidente de Vercel dejó al descubierto y cómo auditarlo en tus integraciones
La mayoría de los developers revisa los scopes OAuth exactamente una vez: el día que configuran la integración. Después de eso, la conexión "funciona" y nadie vuelve a mirarla. Sí, leíste bien. Y eso no es descuido puntual — es el patrón por defecto de la industria. Y es exactamente el vector que casos como el incidente de Vercel con GitHub OAuth pusieron en evidencia.
Mi tesis es directa: el incidente de Vercel no fue una vulnerabilidad técnica en el sentido clásico. Fue un fallo de principio de mínimo privilegio aplicado a OAuth. La seguridad de las integraciones de terceros es tan buena como el scope que les otorgaste — y la mayoría de los developers no revisan eso una vez que la integración funciona. Ese gap entre "funciona" y "está bien diseñado" es el problema que quiero desarmar acá.
Qué es el scope creep en OAuth 2.0 y por qué importa ahora
El scope creep en OAuth no es un CVE. No aparece en un scanner de vulnerabilidades. Es un proceso gradual: una integración de terceros arranca pidiendo lo mínimo necesario y, con el tiempo — por conveniencia, por copy-paste de documentación, por falta de revisión — termina acumulando permisos que van mucho más allá de su función original.
El RFC 6819 — OAuth 2.0 Threat Model and Security Considerations lo documenta explícitamente como una amenaza de superficie de ataque: si un token con scopes excesivos se ve comprometido, el radio de daño es proporcional a lo que ese token puede hacer, no a lo que debería hacer. El RFC nombra esto en la sección 4.1.2 como scope elevation — uno de los vectores de ataque que los authorization servers y los clients deben mitigar activamente.
En el caso de Vercel y GitHub, la discusión pública giró alrededor de qué acceso tenía la app de GitHub autorizada a actuar en nombre del usuario — y si ese acceso era proporcional a la función declarada. No voy a reconstruir el incidente completo acá (el video de OAuth Supply-Chain Risk que publiqué cubre ese gancho). Lo que me interesa desarrollar es la clase de problema que representa: un tercero que tiene más permisos de los que necesita, y nadie que los audite de forma periódica.
Lo incómodo: la mayoría de los sistemas de CI/CD, hosting y observabilidad que usás hoy tienen acceso OAuth a repositorios, registros de contenedores o APIs de deployment. ¿Sabés exactamente qué scopes tienen autorizados? ¿Cuándo fue la última vez que los revisaste?
RFC 6819: qué dice y qué no dice
El RFC 6819 es la base técnica para entender el modelo de amenazas de OAuth 2.0. Algunos puntos concretos que son relevantes para este análisis:
Lo que sí dice:
- Los clients deben pedir el scope mínimo necesario para su función (principio de mínimo privilegio, sección 3.1).
- Los authorization servers deben implementar mecanismos para que los usuarios puedan revisar y revocar tokens activos.
- El scope de un token comprometido determina directamente el radio de daño de un ataque.
- La acumulación de scopes a lo largo del tiempo (scope creep) incrementa la superficie de ataque sin incrementar la funcionalidad declarada.
Lo que no dice — y es importante aclarar:
- El RFC no especifica cómo implementar auditoría de scopes en producción. Eso queda en la capa de aplicación.
- No define frecuencia de rotación de tokens ni políticas de revocación automática. Eso depende del authorization server y de la política de cada organización.
- No resuelve el problema de los refresh tokens de larga duración — que en muchas integraciones son prácticamente equivalentes a credenciales permanentes.
El RFC es el mapa del territorio. El "cómo auditarlo en práctica" es responsabilidad del equipo que diseñó la integración.
Dónde se equivoca la gente: la receta común y su costo oculto
El patrón que veo repetirse en integraciones OAuth tiene tres momentos:
1. El setup inicial con scopes generosos
Cuando integrás un servicio de terceros — una plataforma de deployment, un sistema de analytics, una herramienta de CI — la documentación oficial casi siempre muestra el ejemplo más amplio. repo en lugar de repo:read. admin:org en lugar de read:org. Es más fácil que funcione. El copy-paste gana.
El problema es que ese scope queda grabado en el token y en la autorización. Y nadie lo ajusta después.
2. La integración "funciona" y desaparece del radar
Una vez que el pipeline está verde, la integración pasa a ser infraestructura invisible. Nadie la toca porque nadie quiere romperla. Esto es lógico desde el punto de vista operativo — y es exactamente el comportamiento que crea scope creep acumulado.
Hay un paralelo directo con la configuración de endpoints de observabilidad: si nunca revisás qué exponés, terminás con más superficie de ataque de la que creés que tenés. Es el mismo principio que apliqué cuando analicé qué exponer y qué ocultar en Spring Boot Actuator.
3. La revocación como reacción, no como proceso
En la mayoría de los equipos, los tokens OAuth se revocan cuando hay un incidente o cuando alguien se va del equipo. No hay un proceso proactivo de revisión. Eso significa que integraciones que ya no existen pueden seguir teniendo tokens activos con scopes amplios — esperando ser usados por alguien que encontró las credenciales.
El costo oculto: si ese token se filtra — por un leak en el log, por un repositorio público que incluyó variables de entorno, por una dependencia comprometida — el atacante tiene acceso proporcional al scope más amplio que el token permite, no al mínimo necesario.
Cómo auditar scopes OAuth en integraciones existentes: checklist accionable
Esta es la parte que la mayoría de los posts omite. No alcanza con entender el problema — necesitás un proceso para revisarlo.
Paso 1: Inventariá todas las integraciones OAuth activas
Para cada integración, respondé:
- ¿Qué scopes tiene autorizados actualmente?
- ¿Cuándo fue la última vez que se usó?
- ¿Sigue siendo necesaria?
En GitHub, podés revisar las apps autorizadas en Settings > Applications > Authorized OAuth Apps. En Google, en myaccount.google.com/permissions. La mayoría de los identity providers tienen una pantalla similar.
Paso 2: Evaluá cada scope contra su función real
Para cada scope, la pregunta es simple pero incómoda: ¿la integración necesita este permiso para hacer lo que declara que hace?
Usá este criterio:
// Criterio de evaluación por scope
type ScopeAuditResult = {
scope: string; // nombre del scope
funcion: string; // para qué lo usa la integración
necesario: boolean; // ¿sin esto deja de funcionar?
alternativa: string; // scope más restrictivo si existe
accion: "mantener" | "reducir" | "revocar";
};
// Ejemplo: integración de CI/CD
const auditCICD: ScopeAuditResult[] = [
{
scope: "repo",
funcion: "leer código para builds",
necesario: false, // solo necesita leer, no escribir
alternativa: "repo:read o contents:read",
accion: "reducir",
},
{
scope: "admin:repo_hook",
funcion: "crear webhooks para trigger de builds",
necesario: true,
alternativa: "write:repo_hook (más específico)",
accion: "reducir",
},
{
scope: "delete_repo",
funcion: "ninguna declarada",
necesario: false,
alternativa: "no necesario",
accion: "revocar",
},
];
Paso 3: Revisá los refresh tokens de larga duración
Un refresh token activo sin expiración es funcionalmente equivalente a una credencial permanente. Preguntá:
- ¿El authorization server de este proveedor soporta refresh token rotation?
- ¿Los tokens tienen expiración configurada?
- ¿Hay algún proceso de rotación periódica?
Si el proveedor no soporta rotación automática, la alternativa es establecer un proceso manual con frecuencia definida — trimestral es razonable para integraciones de producción.
Paso 4: Implementá detección de uso anómalo
Un scope autorizado que nunca se usa es candidato directo a revocación. Si el authorization server expone logs de uso por scope (algunos lo hacen), revisalos. Si no, implementá logging propio en la capa de integración:
// Middleware de logging para integraciones OAuth en Next.js
// Registra qué scopes se usan realmente en producción
import { NextRequest, NextResponse } from "next/server";
export async function middleware(req: NextRequest) {
const authHeader = req.headers.get("authorization");
if (authHeader?.startsWith("Bearer ")) {
// Registrá el endpoint que requirió el token
// para mapear uso real vs scopes autorizados
console.log(
JSON.stringify({
timestamp: new Date().toISOString(),
path: req.nextUrl.pathname,
method: req.method,
// No loguear el token completo — solo el jti si está disponible
tokenPresente: true,
})
);
}
return NextResponse.next();
}
export const config = {
// Aplicar solo a rutas que consumen APIs externas OAuth
matcher: ["/api/integraciones/:path*"],
};
Paso 5: Definí un proceso de revisión periódica
Sin proceso, la auditoría es un evento único. El scope creep es un proceso continuo. Necesitás que se encuentren:
- Frecuencia mínima sugerida: cada 90 días para integraciones activas, cada 30 días para integraciones con scopes amplios.
- Trigger obligatorio: cualquier cambio de equipo (alta o baja de developer) debe disparar una revisión de tokens activos.
- Dueño definido: alguien en el equipo tiene que ser responsable del inventario de integraciones OAuth. Si no hay dueño, no hay proceso.
Controles arquitecturales: prevenir antes que auditar
La auditoría reactiva es necesaria. Pero hay controles que podés incorporar al diseño desde el principio:
1. Proxy de autorización interno
En lugar de que cada servicio maneje directamente tokens OAuth de terceros, podés centralizar en un proxy interno que actúa como authorization intermediary. El proxy valida scopes, loguea uso y puede revocar sin cambiar la integración downstream. Es más infraestructura, pero el control centralizado vale la complejidad en sistemas con muchas integraciones.
2. Token binding por contexto
Si el authorization server del proveedor lo soporta, vinculá los tokens a contextos específicos (IP range, user agent, recurso). Esto no elimina el scope creep pero reduce el radio de daño de un token comprometido.
3. Scopes granulares desde el día uno
La decisión más barata es la primera: pedir el scope más restrictivo posible en el setup inicial. Si después necesitás más permisos, los pedís. El costo de pedir menos y ajustar es mucho menor que el costo de auditar y revocar después.
Esto conecta con el principio de superficie mínima que aplico en otras capas — desde los traces de OpenTelemetry que cruzan el edge hasta los controles que evaluás antes de exponer endpoints de observabilidad. El patrón es consistente: menos superficie expuesta, menor radio de daño posible.
4. Alerta sobre scopes no utilizados
Si podés instrumentar el uso de scopes (ver paso 4 del checklist), configurá una alerta cuando un scope autorizado no registra uso en 30 días. Eso es un candidato directo a revisión y posible revocación.
Los límites de este análisis: qué no podés concluir sin más datos
Sería deshonesto de mi parte presentar esto como una guía completa sin marcar los límites:
- No tengo acceso a los detalles internos del incidente de Vercel. El análisis está basado en la discusión pública y en el modelo de amenazas del RFC 6819. Si la causa raíz fue diferente, el diagnóstico cambia.
- El RFC 6819 documenta amenazas pero no prescribe implementaciones. Lo que considerás "scope mínimo necesario" depende del contexto específico de cada integración — no hay un número universal.
- La frecuencia de auditoría que propongo (90 días) no tiene respaldo en investigación formal. Es un criterio de oficio razonable, no una métrica validada estadísticamente. Ajustalo según el nivel de riesgo de cada integración.
- Los refresh tokens de larga duración son un riesgo real, pero el impacto concreto depende del proveedor. Algunos authorization servers implementan rotación automática y detección de reuse; otros no. Revisá la documentación específica del proveedor antes de asumir el peor caso.
FAQ: OAuth scope creep y auditoría de integraciones
¿Qué diferencia hay entre scope creep y privilege escalation en OAuth?
El privilege escalation es un ataque activo donde alguien intenta obtener permisos que no tiene. El scope creep es un proceso pasivo: los permisos ya fueron otorgados legítimamente, pero se acumularon más allá de lo necesario. El RFC 6819 los trata como amenazas distintas — el primero en sección 4.1.3, el segundo como consecuencia del incumplimiento del principio de mínimo privilegio.
¿Cómo sabés qué scopes realmente necesita una integración?
El criterio más práctico: empezá con el scope más restrictivo que la documentación del proveedor ofrece, intentá que la integración funcione con eso, y expandí solo cuando encontrés un error de autorización concreto. La mayoría de los problemas de scope creep vienen del approach inverso: empezar con todo y nunca revisar.
¿Los tokens de OAuth con refresh token son más riesgosos que los de acceso directo?
En términos de duración del riesgo, sí. Un access token expirado en 1 hora limita el daño a esa ventana. Un refresh token sin expiración (o con expiración de meses) actúa como credencial semi-permanente. El RFC 6819 en sección 4.1.2 recomienda explícitamente implementar refresh token rotation y detección de reuse sospechoso.
¿Vale la pena implementar un proxy de autorización para integraciones de terceros?
Depende del número de integraciones y del nivel de riesgo. Para un proyecto personal con dos integraciones OAuth, no. Para un sistema con decenas de integraciones activas que manejan datos sensibles, el proxy centralizado es un control razonable. El costo de implementarlo es real — no lo subestimes.
¿Qué pasa si revocás un token de una integración activa?
La integración deja de funcionar hasta que el usuario vuelva a autorizar. En integraciones de CI/CD o deployment, eso puede bloquear el pipeline. Por eso la revocación tiene que ir acompañada de un proceso de re-autorización planificado, no como reacción de emergencia.
¿El scope creep aplica también a integraciones internas (entre servicios propios)?
Absolutamente. Si usás OAuth entre microservicios propios — lo que se conoce como machine-to-machine con client credentials flow — el mismo principio aplica. Cada servicio debe tener exactamente el scope necesario para su función. Un servicio de notificaciones no necesita scope de escritura sobre datos de usuario. Este vector es menos visible que el de terceros pero igualmente relevante.
Cierre: la deuda de permisos que nadie mide
Hay un tipo de deuda técnica que no aparece en ningún backlog: la deuda de permisos. Integraciones configuradas con scopes amplios porque era más rápido, tokens que nunca se revisaron porque "funcionan", refresh tokens activos de servicios que ya no existen.
El incidente de Vercel es útil no porque sea único — es útil porque es público y documentado. El mismo patrón existe en casi cualquier sistema con más de cinco integraciones OAuth activas. La diferencia entre el que tuvo el incidente y el que no lo tuvo todavía es, en muchos casos, solo cuestión de tiempo y de qué integración se vio comprometida primero.
Lo que me parece honesto decirte: no existe una herramienta que resuelva esto por vos. El RFC 6819 te da el modelo de amenazas. El checklist de arriba te da el proceso. Pero la decisión de cuándo auditar, qué revocar y cómo diseñar el scope mínimo para cada integración es criterio técnico — del tipo que se construye con oficio y con la incomodidad de revisar lo que ya funciona.
Mi recomendación práctica: abrí ahora mismo la pantalla de apps autorizadas de tu GitHub, tu Google Workspace o tu identity provider principal. Contá cuántas integraciones tienen scopes que no reconocés como necesarios. Si el número te incomoda, ya tenés el primer paso.
Fuente original:
- OAuth 2.0 Threat Model and Security Considerations (RFC 6819): https://datatracker.ietf.org/doc/html/rfc6819
Este artículo fue publicado originalmente en juanchi.dev
Top comments (0)