Article

Los roles de su cuenta de servicio no son los roles que usa

Encontramos una cuenta de servicio OAuth con el rol de administrador y esperábamos hallar una fuga de privilegios. No la había: simple_oauth había estado ignorando ese rol todo el tiempo. Luego casi tumbamos el sitio al demostrarlo.
July 15, 2026
Topics:SecurityAccess ControlHeadless CMS
Tags:DrupalPHPJSON:API

Nuestro front end desacoplado se autentica ante Drupal como una cuenta de servicio mediante OAuth client_credentials. Durante una revisión notamos que esa cuenta tenía el rol de administrador.

Ese es el tipo de cosa que detiene una revisión. Un front end de cara al público anónimo que posee un token emitido desde una cuenta de administrador tiene exactamente la forma de un hallazgo grave. Así que fuimos a buscar la fuga.

No había ninguna. Y entender por qué resultó más útil de lo que habría sido la vulnerabilidad.

El token no lleva los roles de la cuenta

En simple_oauth con granularidad por rol, el alcance es el límite de seguridad, no el usuario. Los roles de la cuenta se eliminan de un token client_credentials. Los permisos que el token realmente lleva son aquellos a los que se asignan sus alcances concedidos, y nada más.

Lo demostramos en lugar de confiar en la documentación, y la prueba más limpia usó una cuenta contra dos consumidores:

  • Mismo usuario, roles []. Alcance previewer → el borrador no publicado se devuelve.
  • Mismo usuario, roles []. Alcance nextjs_sitenull.

Cuenta idéntica. Alcance distinto. Respuesta distinta. El usuario no es lo que decide.

Lo que significa que el rol administrator en esa cuenta nunca le estaba concediendo nada al front end. Era inerte. El hallazgo era real —el rol no debería haber estado ahí—, pero su gravedad no era la que aparentaba. Era un arma cargada sin percutor: inofensiva hoy, y a un cambio de configuración de dejar de serlo. Lo eliminamos.

La comprobación que nos habría mentido

Antes de eliminar el rol en producción, queríamos un control. El plan obvio: bloquear la cuenta de servicio y confirmar que el front end se rompe de la forma que predecimos. Lo probamos primero en un entorno de desarrollo.

Menos mal. Esto es lo que hace una cuenta de servicio bloqueada:

  • La emisión de un token sigue teniendo éxito. Recibe un 200 y un token de acceso real. Todas las comprobaciones de salud que se le ocurriría escribir pasan.
  • Todo uso de ese token devuelve 401. Todas las solicitudes de contenido del front end fallan.

En producción eso es el sitio web público quedándose a oscuras, mientras la comprobación de credenciales que usted configuró para detectar exactamente esto informa en verde.

El detalle incómodo: nuestro propio primer borrador del plan de verificación decía "confirmar que la emisión del token sigue teniendo éxito". Esa comprobación pasa con una cuenta bloqueada. Escribimos una prueba que habría certificado la interrupción como saludable. La prueba no estaba equivocada sobre lo que medía. Medía lo equivocado: la emisión, cuando lo que nos importaba era el uso.

Si se lleva una regla operativa de esto: nunca verifique una credencial emitiendo un token. Verifíquela gastando uno. La autenticación y la autorización fallan en momentos distintos, y en la brecha entre ambas es donde vive la falsa confianza.

Pregunte al token qué cree que es

La otra trampa por el camino fue leer una señal completamente equivocada. Consultamos /jsonapi/user/user con el token de servicio, obtuvimos un 200 y por un momento creímos haber confirmado la fuga.

No lo habíamos hecho. JSON:API no devuelve 403 para colecciones que usted no puede leer por completo: filtra por acceso y devuelve lo que está autorizado a ver. Un 200 es la respuesta normal para una solicitud que no encontró nada privilegiado. El control lo zanja en un comando: anónimo, sin ningún token, también recibe un 200 en ese punto de acceso. Un resultado que una solicitud sin credenciales reproduce no es evidencia sobre sus credenciales.

El instrumento correcto era /oauth/debug, que informa de aquello a lo que el propio token se resuelve:

roles: ["authenticated"]

No administrator. El token nunca lo tuvo. Esa es la respuesta a la pregunta real, y bastó una solicitud para obtenerla una vez que preguntamos al punto de acceso correcto.

Qué cambiamos

El rol se retiró de la cuenta mediante un hook post_update, para que el cambio viaje con un despliegue en lugar de vivir en el recuerdo de alguien de una sesión de administración en producción. Cada consumidor tiene ahora un alcance con una única función: el cliente público puede leer contenido publicado, y el cliente de vista previa puede leer borradores no publicados. Un error en una página pública no puede filtrar un borrador, porque el cliente que renderiza las páginas públicas no tiene ningún alcance que pueda ver uno.

Nada de esto era una vulnerabilidad. Era peor, de un modo más silencioso: un sistema cuyas reglas de acceso reales vivían en un lugar distinto de donde todos miraban. La cuenta decía administrador. El token decía autenticado. Solo uno de ellos estaba en la ruta de la solicitud, y no era el visible en la interfaz de usuario.