Seguimiento de nan#29, cerrado por cloud-api#120. Aquel PR impide operar con un alias colisionante, pero deja dos cabos.
1. El alias colisionante se sigue pudiendo crear
Ni el link de Discord (internal/handlers/account_discord.go:198) ni el trigger members_auto_handle (migración 018) consultan el allowlist. Así que el alias se crea igual y el miembro solo se entera al pulsar el botón de su key, recibiendo un 409 que no puede resolver por sí mismo.
El caso del handle es el peor de los dos: se autogenera del local-part del email, así que un alta con ccordova@… produce el alias reservado sin que el miembro haga nada, y UpdateMemberProfile (internal/db/queries/members.sql:282) excluye explícitamente handle, o sea que no hay forma de cambiarlo desde el portal.
Arreglo propuesto: consultar el allowlist en el punto donde el alias se elige (trigger de handle y link de Discord), en vez de solo donde se usa. El bucle de colisiones del trigger ya evita chocar con otros handles; le falta chocar con la lista de service keys.
2. Las métricas resuelven el alias por su cuenta, y de forma más ancha
MetricsHandler.resolveKeyAlias (internal/handlers/metrics.go:100) resuelve el alias sin pasar por el guard de resolveUser, y internal/metrics/litellmdb.go:124 compara case-insensitive:
LOWER(v.key_alias) = LOWER($1)
Eso es más ancho que la igualdad exacta del guard. Un miembro con alias colisionante vería en su dashboard el consumo de tokens y el gasto de la key de servicio. Arrastra por la misma resolución a metrics_sse.go y a internal/sync/usage.go.
Es solo lectura y agregado, y es preexistente, pero es el hueco que queda tras el #120.
3. Detección proactiva, si se quiere
Sin recuperar el WARN (que se bajó a Debug porque resolveUser corre en cada poll del portal), la alerta barata es:
HTTPRequestsTotal{method="GET", route="/api/keys", status="409"} > 0
Hoy ese 409 en GET /api/keys solo puede venir de este guard, así que aísla el caso con precisión y avisa del miembro encerrado sin ensuciar los logs. En POST comparte código con "API key already exists", así que ahí no discrimina.
Lo que se decidió NO arreglar
El guard corre antes de ensureEntitled, lo que lo convierte en un oráculo de enumeración del allowlist. Se deja así a propósito: explotarlo exige estar autenticado y cambiar el alias por intento, el premio es nulo (conocer el nombre de una service key no da acceso a nada, el allowlist la protege en todas las rutas mutantes), y el arreglo obvio es peor — mover el guard detrás de ensureEntitled haría que un miembro community viera el paywall en vez de la colisión, con la colisión esperándole intacta el día que pague.
Seguimiento de nan#29, cerrado por cloud-api#120. Aquel PR impide operar con un alias colisionante, pero deja dos cabos.
1. El alias colisionante se sigue pudiendo crear
Ni el link de Discord (
internal/handlers/account_discord.go:198) ni el triggermembers_auto_handle(migración 018) consultan el allowlist. Así que el alias se crea igual y el miembro solo se entera al pulsar el botón de su key, recibiendo un 409 que no puede resolver por sí mismo.El caso del handle es el peor de los dos: se autogenera del local-part del email, así que un alta con
ccordova@…produce el alias reservado sin que el miembro haga nada, yUpdateMemberProfile(internal/db/queries/members.sql:282) excluye explícitamentehandle, o sea que no hay forma de cambiarlo desde el portal.Arreglo propuesto: consultar el allowlist en el punto donde el alias se elige (trigger de handle y link de Discord), en vez de solo donde se usa. El bucle de colisiones del trigger ya evita chocar con otros handles; le falta chocar con la lista de service keys.
2. Las métricas resuelven el alias por su cuenta, y de forma más ancha
MetricsHandler.resolveKeyAlias(internal/handlers/metrics.go:100) resuelve el alias sin pasar por el guard deresolveUser, yinternal/metrics/litellmdb.go:124compara case-insensitive:Eso es más ancho que la igualdad exacta del guard. Un miembro con alias colisionante vería en su dashboard el consumo de tokens y el gasto de la key de servicio. Arrastra por la misma resolución a
metrics_sse.goy ainternal/sync/usage.go.Es solo lectura y agregado, y es preexistente, pero es el hueco que queda tras el #120.
3. Detección proactiva, si se quiere
Sin recuperar el
WARN(que se bajó aDebugporqueresolveUsercorre en cada poll del portal), la alerta barata es:Hoy ese 409 en
GET /api/keyssolo puede venir de este guard, así que aísla el caso con precisión y avisa del miembro encerrado sin ensuciar los logs. EnPOSTcomparte código con "API key already exists", así que ahí no discrimina.Lo que se decidió NO arreglar
El guard corre antes de
ensureEntitled, lo que lo convierte en un oráculo de enumeración del allowlist. Se deja así a propósito: explotarlo exige estar autenticado y cambiar el alias por intento, el premio es nulo (conocer el nombre de una service key no da acceso a nada, el allowlist la protege en todas las rutas mutantes), y el arreglo obvio es peor — mover el guard detrás deensureEntitledharía que un miembrocommunityviera el paywall en vez de la colisión, con la colisión esperándole intacta el día que pague.