Ejecute estas cinco pruebas contra un despliegue de staging de su API GraphQL antes de que el endpoint sea público. Configure staging tal como estará producción, o los resultados valdrán poco.
Siguen las cuatro áreas de la GraphQL Cheat Sheet de OWASP: validación de entrada, consultas costosas, control de acceso y configuración por defecto insegura.
1. Compruebe los valores por defecto
Los errores excesivamente detallados, la introspección y GraphiQL son los valores por defecto inseguros más habituales. Pruebe los tres. Empiece con la consulta de introspección más pequeña:
{__schema{types{name}}}
Compruebe si el servidor responde con detalles del esquema o si la rechaza. Después, busque GraphiQL en un navegador. Por último, envíe una consulta mal formada y otra con un argumento erróneo, y lea el error. Nombres de resolvers, stack traces o mensajes de la base de datos en la respuesta son un mapa para un atacante.
La solución es de configuración. Desactive la introspección y GraphiQL allí donde la API no esté pensada para explorarse, devuelva errores genéricos y deje el detalle en los logs del servidor.
2. Pase entrada hostil por cada argumento
El usuario aporta identificadores, y los data fetchers los utilizan después en peticiones HTTP, consultas a bases de datos y otras llamadas. Ahí es donde se produce la inyección. Pruebe cada argumento de cada query y mutation con los mismos payloads que usaría en cualquier otra API: inyección SQL y NoSQL, inyección de comandos del sistema operativo, SSRF e inyección CRLF.
Para poder automatizarlo, genere las listas de argumentos a partir del esquema mientras la introspección sigue activa en staging, recorra con un bucle sus payloads sobre ellas y desactive la introspección después.
Qué debe imponer:
- Tipos específicos de GraphQL, como escalares y enums, y esquemas definidos para la entrada de las mutations.
- Una lista de permitidos de caracteres, no una lista de bloqueo. La entrada alfanumérica y no unicode es un buen punto de partida, porque descarta muchos ataques.
- Sentencias parametrizadas para todo lo que se pase a un intérprete. Si utiliza un ORM u ODM, úselo tal como indica su documentación, ya que la inyección a través del ORM sigue siendo posible.
- Rechazo de la entrada no válida sin revelar cómo funciona la validación.
3. Encuentre la consulta costosa
Una API es vulnerable si falta algún límite de recursos o está mal configurado. La entrada de OWASP sobre el consumo de recursos sin restricciones enumera los tiempos máximos de ejecución, la memoria máxima asignable, los descriptores de archivo, los procesos, el tamaño máximo de los archivos subidos, los registros por página, las operaciones por petición de cliente y los límites de gasto en proveedores externos.
Para probarlo, construya la consulta más profunda que permita su esquema siguiendo relaciones en un bucle. Adapte los nombres a su esquema:
{ users { orders { customer { orders { customer { email } } } } } }
Siga añadiendo niveles hasta que el servidor la rechace o se ralentice. Después, solicite un tamaño de página muy grande y envíe un payload de tamaño excesivo.
El consejo de la cheat sheet es añadir comprobaciones que limiten o impidan las consultas demasiado costosas. Combínelo con un tope de tamaño de página, un tiempo máximo de ejecución y tamaños máximos para cadenas, arrays y subidas de archivos. Ajuste los valores a partir de una semana de logs de tráfico real, de modo que los clientes legítimos queden por debajo.
4. Esquive el rate limiter con batching
El batching es fuerza bruta con el número de peticiones oculto. En el segundo escenario de la entrada de OWASP, una mutation de subida de archivos está protegida por un limitador de peticiones (rate limiter) y una comprobación de tamaño. El atacante envía un único POST a /graphql con un array de casi mil operaciones. El limitador ve una sola petición y el servidor se queda sin memoria. Si su API acepta subidas de archivos, nuestro artículo sobre cómo diseñar y probar subidas de archivos seguras cubre ese aspecto.
Pruebe lo mismo contra una mutation de login o de OTP:
[
{"query": "mutation {login(user: \"alice\", pass: \"guess1\") {token}}"},
{"query": "mutation {login(user: \"alice\", pass: \"guess2\") {token}}"}
]
Compruebe si su límite cuenta cada operación del array y si los intentos fallidos siguen llegando a su lógica de bloqueo. Después, pruebe cualquier otra forma que admita su servidor de agrupar varias operaciones en una sola petición.
Limite el número de operaciones por petición de cliente y restrinja la frecuencia con que un mismo cliente puede ejecutar una operación sensible concreta, como la validación de un OTP. Limite también el gasto: en el primer escenario de la entrada, un script desencadena decenas de miles de SMS de pago a través de un flujo de recuperación de contraseña. Establezca límites de gasto con los proveedores externos y alertas de facturación donde no sea posible fijar límites.
5. Pida el campo de otro usuario
La autenticación le dice quién llama. No decide qué campos puede leer. La guía de autorización de GraphQL indica que la ejecución debe comenzar después de que el middleware de autenticación confirme la identidad, y que el servidor debe seguir decidiendo, para cada campo solicitado, si el usuario puede verlo.
Pruebe con dos cuentas normales. Con la sesión de la cuenta A, solicite por ID los objetos de la cuenta B y, después, acceda a ellos a través de un objeto relacionado que ambas cuentas puedan ver. Adapte los nombres a su esquema:
# Direct lookup: should be refused for account A
{ customer(id: "CUSTOMER_ID_OF_B") { email } }
# Same data through a shared parent: must also be refused
{ product(id: "SHARED_PRODUCT_ID") { orders { customer { email } } } }
Si la primera consulta se rechaza y la segunda devuelve el email de B, la comprobación está en la consulta de primer nivel y nunca se aplicó al tipo anidado. Repita la prueba sin ninguna sesión. Si los mismos datos se sirven a través de REST o de una API móvil, pruebe también esos puntos de entrada. Nuestro artículo sobre cómo probar todos los ID de su aplicación frente a IDOR y BOLA cubre el mismo enfoque más allá de GraphQL.
La autorización dentro de un resolver de campo es válida para prototipos, pero obliga a duplicarla en cada punto de entrada. Si las copias divergen, los usuarios ven datos distintos según la API que utilicen. Traslade la lógica a la capa de lógica de negocio para que exista una única fuente de verdad. En el ejemplo de la guía, postRepository.getBody recibe el usuario y la publicación y devuelve el cuerpo únicamente al autor. Pase un objeto de usuario completo (hydrated), no un token opaco ni una clave de API.
Lo que un escáner no le dirá
Las herramientas automáticas aportan amplitud, pero la OWASP Web Security Testing Guide señala que están diseñadas para aplicaciones en general, no para su código a medida y sus reglas de negocio. Los fallos más graves suelen estar entrelazados con la lógica de negocio.
Las pruebas 1 a 3 son en su mayor parte automatizables. La prueba 5 requiere trabajo manual, porque alguien tiene que entender qué tenant es propietario de cada objeto, por lo que es la que más conviene encargar a un equipo externo. Nuestro servicio de pentesting la cubre.
Por dónde empezar
- Envíe la consulta de introspección y abra GraphiQL en el despliegue de staging. Desactive lo que responda y vuelva a probar con una consulta mal formada para comprobar los errores.
- Haga una lista de todos los argumentos de queries y mutations. Confirme que cada uno tiene un tipo específico o un enum y un validador basado en lista de permitidos.
- Envíe la consulta con anidamiento profundo, una petición con un tamaño de página grande y un payload de tamaño excesivo. Establezca los límites, vuelva a enviar las consultas y confirme que se rechazan todas.
- Envíe un batch en array contra sus mutations de login y OTP. Imponga un límite por petición y sobre las repeticiones de operaciones sensibles.
- Ejecute la prueba con dos cuentas, incluidas las relaciones anidadas, en cada tipo que contenga datos personales o comerciales. Traslade a la capa de lógica de negocio cualquier comprobación que encuentre en un resolver.
