Cómo defenderse de los ataques a GraphQL: un análisis a fondo de las vulnerabilidades más comunes
Este artículo analiza en profundidad las vulnerabilidades más comunes de GraphQL, por qué se producen y cómo pueden mitigarse.
Cómo defenderse de los ataques a GraphQL: un análisis a fondo de las vulnerabilidades más comunes
GraphQL ha revolucionado el desarrollo de API con su enfoque flexible y eficiente para la consulta de datos. Sin embargo, como cualquier otra tecnología, presenta sus propios desafíos de seguridad.
Este artículo refleja nuestra experiencia en Ostorlab en la automatización de la detección y las pruebas de vulnerabilidades de GraphQL.
Analizaremos en profundidad las vulnerabilidades más comunes de GraphQL, por qué se producen y cómo pueden mitigarse.
¿Qué es GraphQL?
GraphQL es un lenguaje de consulta para API y un tiempo de ejecución para ejecutar esas consultas. Permite a los clientes solicitar exactamente los datos que necesitan, ni más ni menos, lo que puede optimizar de forma significativa las interacciones con la API.
Desarrollado originalmente por Facebook en 2012 y liberado como código abierto en 2015, GraphQL se diseñó para superar las limitaciones de las API REST tradicionales, como la obtención excesiva o insuficiente de datos.
Según Wappalyzer, más de 176,000 sitios web utilizan GraphQL en la actualidad. Esta cifra crece rápidamente a medida que más empresas adoptan GraphQL, entre ellas AWS, PayPal y GitHub. Consulte GraphQL Landscape para ver la lista completa.

query CurrentUser {
currentUser {
name
age
}
}
Respuesta:
{
"currentUser":{
"name":"John Doe",
"age":23
}
}
Características principales
- Un único endpoint: las API de GraphQL exponen un único endpoint a través del cual se realizan todas las interacciones.
- Obtención precisa de datos: los clientes solicitan exactamente lo que necesitan, lo que reduce la sobrecarga de red.
- Esquema fuertemente tipado: un esquema bien definido especifica los tipos y las relaciones, lo que permite consultas potentes. La integridad del esquema y la seguridad de tipos integradas de GraphQL eliminan la necesidad de versionar los datos.

Tipos en GraphQL
Los esquemas de GraphQL se construyen sobre tres tipos principales:
- Tipos raíz
- Tipos escalares
- Tipos de objeto

Tipos raíz
1. Queries: recuperan u obtienen datos del servidor.
2. Mutations: modifican o manipulan datos (crear, actualizar, eliminar).
3. Subscriptions: permiten a los clientes recibir actualizaciones de datos en tiempo real.

Tipos escalares
Los tipos escalares representan valores simples como enteros, cadenas de texto, booleanos, etc. Son los bloques de construcción básicos de un esquema.
Tipos de objeto
Los tipos de objeto representan entidades complejas con múltiples campos. Estos campos pueden ser escalares u otros tipos de objeto. Por ejemplo:
type Human {
id: String
name: String
homePlanet: Planet
}
type Planet {
id: String
name: String
}

Descubrimiento del esquema de GraphQL e introspección.
GraphQL ofrece introspección, que permite a los desarrolladores consultar el esquema para conocer los tipos, las queries y las mutations disponibles (puede considerarse el equivalente a una solicitud OPTIONS en REST).
Aunque la introspección no es un problema de seguridad en sí misma y resulta útil durante el desarrollo, los atacantes pueden aprovecharla para comprender mejor las capacidades de la API y, potencialmente, abusar de su API de GraphQL.

La introspección está habilitada
Ejemplo de una consulta de introspección para obtener todas las mutations:
Solicitud
{
__schema {
mutationType {
kind
name
fields {
name
description
deprecationReason
}
}
}
}
Respuesta
{
"__schema":{
"mutationType":{
"kind":"OBJECT",
"name":"Mutation",
"fields":[
{
"name":"createUser",
"description":"Create a new user"
}
]
}
}
}
Ejemplo de introspección que puede volcar el esquema completo, incluidas las queries, las mutations y los objectTypes:
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
fields(includeDeprecated: true) {
name
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
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
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
}
}
Cómo eludir los filtros de expresiones regulares sobre `__schema`
Cuando los desarrolladores deshabilitan la introspección, pueden usar una expresión regular para excluir la palabra clave __schema de las consultas. Puede probar caracteres como espacios, saltos de línea y comas, ya que GraphQL los ignora, pero los filtros de expresiones regulares defectuosos no.
Por ejemplo, si el desarrollador solo ha excluido __schema{, la siguiente consulta de introspección, con un salto de línea después de __schema, no quedaría excluida:
{
"query": "query{__schema
{queryType{name}}}"
}
Esquema a partir de sugerencias y del manejo de errores.
A veces, incluso con la introspección deshabilitada, es posible inferir partes del esquema de GraphQL aprovechando las sugerencias y los mensajes de error que proporciona el servidor. Por ejemplo, cuando una solicitud contiene un campo mal escrito pero cercano a un campo existente, el servidor GraphQL puede devolver un error que sugiere el nombre correcto del campo.

Ejemplo:

Lo mismo ocurre al inferir mutations y argumentos.

Cómo deshabilitar la introspección
Cuando sea posible, la introspección debe deshabilitarse en los entornos de producción y habilitarse únicamente durante el desarrollo. Según la especificación de GraphQL:
"All types and directives defined within a schema must not have a name that begins with __ (two underscores), as this is reserved exclusively for GraphQL's introspection system."
Un enfoque habitual para deshabilitar la introspección consiste en excluir los campos que empiezan por __.
Este método limita eficazmente la introspección, pero es fundamental aplicar esta configuración de forma coherente en todos los entornos.
Ataques de denegación de servicio (DoS)
En esta sección analizaremos en profundidad diversos ataques de denegación de servicio (DoS) que los atacantes pueden emplear (y emplearán) contra su aplicación.
GraphQL expone los datos de la aplicación como un grafo, lo que permite a los clientes recuperar datos recorriendo las relaciones entre los nodos (tipos).
La mayoría de las vulnerabilidades que se enumeran a continuación se deben a la parte de grafo de GraphQL.
Fragmentos circulares (severidad baja)
Los fragmentos de GraphQL permiten reutilizar la lógica de las consultas mediante la definición de campos reutilizables.
Sin embargo, los fragmentos circulares pueden utilizarse para construir consultas que consumen una cantidad excesiva de recursos del servidor.
Ejemplo de fragmento circular:
fragment UserFields on User {
comments {
...CommentFields
}
}
fragment CommentFields on Comment {
owner {
...UserFields
}
}
Como puede observar, el fragmento UserFields hace referencia a CommentFields, y CommentFields hace referencia de nuevo a UserFields, lo que provocará un bucle infinito.

Los servidores GraphQL suelen identificar y rechazar las consultas con referencias cíclicas entre fragmentos, y devuelven un mensaje de error.
Sin embargo, en algunos casos la implementación del servidor puede no ajustarse estrictamente a la especificación de GraphQL.
Por lo tanto, es necesario adoptar medidas adicionales:
Detección de fragmentos circulares: utilice herramientas de análisis de esquemas como GraphQL-ESLint para detectar y evitar los fragmentos circulares.
Implemente un análisis del coste de las consultas: asigne costes a los campos y las consultas y rechace las que superen un límite predefinido. Por ejemplo, si utiliza Apollo server, puede usar la biblioteca graphql-query-complexity:
const {
queryComplexity,
simpleEstimator
} = require('graphql-query-complexity');
const server = new ApolloServer({
schema,
validationRules: [
queryComplexity({
estimators: [
simpleEstimator({
defaultComplexity: 1
})
],
maximumComplexity: 1000, // Reject queries that exceed this cost
}),
],
});
Sobrecarga de alias (severidad baja)
La sobrecarga de alias en GraphQL se produce cuando un atacante utiliza una gran cantidad de alias en una consulta para saturar la capacidad de procesamiento del servidor.
En GraphQL, los alias permiten a los clientes solicitar el mismo campo varias veces con nombres diferentes. Sin embargo, el uso excesivo de alias en una sola consulta puede provocar ataques de denegación de servicio (DoS) al agotar los recursos del servidor. Este ataque puede degradar el rendimiento o causar interrupciones completas del servicio.
Por ejemplo:
query AliasOverLoading {
alias1: __typename
alias2: __typename
alias3: __typename
alias4: __typename
alias5: __typename
}
Impacto en la seguridad de la sobrecarga de alias:
La sobrecarga de alias plantea riesgos de seguridad significativos para las API de GraphQL. Una de las principales consecuencias es la denegación de servicio (DoS), en la que una consulta con un número excesivo de alias obliga al servidor a asignar recursos desproporcionados para procesarla y responder. Esto puede ralentizar o incluso bloquear el servicio y afectar a su disponibilidad. Además, el servidor puede sufrir agotamiento de recursos, ya que gestionar numerosos alias de forma simultánea puede provocar el agotamiento de la memoria, picos de CPU o un rendimiento gravemente degradado.
Pueden implementarse varias estrategias para mitigar el riesgo de sobrecarga de alias. En primer lugar, aplicar tiempos de espera a las consultas es una medida eficaz. Al establecer un tiempo máximo de ejecución, el servidor puede finalizar automáticamente las consultas que tardan demasiado en resolverse, lo que evita que las consultas maliciosas sobrecarguen los recursos y provoquen un DoS.
Otra medida importante consiste en limitar el número de alias, como se explicó en la sección anterior.
Referencias circulares (severidad media)

Un atacante puede aprovechar esto para construir una consulta recursiva que sature los recursos del servidor y cause una denegación de servicio y el agotamiento de recursos.
Al probar el impacto de esta vulnerabilidad (en un objetivo real), con Django, graphene-python y una instancia de Cloud SQL con 42 GB de memoria, 8 vCPU y 2.2 TB de almacenamiento SSD.
Logramos construir una consulta recursiva que hizo que nuestra instancia de Cloud SQL alcanzara el 100% de uso de CPU, y el problema persistió hasta que tuvimos que terminar manualmente esas transacciones SQL bloqueadas,

Supongamos que queremos exponer a un usuario junto con la lista de comentarios que ha realizado, mediante los siguientes tipos:
type User {
id: ID!
comments: Comment
username: String
}
type Comment {
id: ID
owner: User!
content: String!
}
El tipo User introduce una referencia circular, que puede utilizarse para crear consultas complejas o recursivas.
Ejemplo de una consulta circular:
query {
user(id: 1) {
id
comments {
id
owner {
id
comments {
id
owner {
id
... # can go forever
}
}
}
}
}
}

Para mitigar el riesgo de referencias circulares en GraphQL, puede:
Refactorizar el esquema para evitar las referencias circulares directas siempre que sea posible. Por ejemplo, utilice un nuevo tipo `LimitedUser` en el tipo `Comment` para romper el bucle:
type User {
id: ID!
username: String!
comments: [Comment]!
}
type LimitedUser {
id: ID!
username: String!
}
type Comment {
id: ID!
owner: LimitedUser!
content: String!
}

Aun así, esta solución puede no ser eficaz, ya que puede romper los clientes y requiere cambios importantes en la base de código.
Aplicar límites de profundidad de las consultas: establezca un límite de la profundidad que pueden alcanzar las consultas para evitar consultas excesivamente profundas.
La mayoría de las implementaciones de servidores GraphQL ofrecen esta función de forma predeterminada; por ejemplo, en Apollo Server:
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
schema,
validationRules: [depthLimit(10)], // Set maximum query depth to 10
});
Fuerza bruta de inicio de sesión mediante agrupación de alias (media)
La fuerza bruta de inicio de sesión mediante agrupación de alias en GraphQL consiste en que un atacante aprovecha la función de alias para automatizar los intentos de inicio de sesión, lo que facilita enviar numerosas combinaciones de credenciales en una sola consulta.
En GraphQL, los alias permiten a los clientes enviar varias versiones de la misma consulta con nombres diferentes. Los atacantes lo aprovechan agrupando solicitudes de inicio de sesión dentro de una sola consulta, con un nombre de alias distinto para cada intento. Esto puede dar lugar a un ataque de fuerza bruta eficiente, que elude las protecciones tradicionales de limitación de tasa y satura el sistema de autenticación con intentos de inicio de sesión.
Ejemplo:
query loginBatch {
login1: login(username: "user1", password: "password1") {
token
}
login2: login(username: "user2", password: "password2") {
token
}
login3: login(username: "user3", password: "password3") {
token
}
...
}
Para mitigar este problema, debe limitar el número de alias permitidos en una sola consulta. Mediante restricciones del lado del servidor o con herramientas como GraphQL Armor, puede limitar el número de alias por solicitud
Limitar el número de intentos fallidos de inicio de sesión por usuario puede ser eficaz para resolver el problema.
Configuración incorrecta de la autorización en GraphQL (severidad alta)
En las API de GraphQL puede producirse una vulnerabilidad grave cuando el acceso a datos sensibles está restringido correctamente en una ruta de consulta, pero queda expuesto en otra debido a comprobaciones de control de acceso incoherentes. Los atacantes pueden recuperar datos no autorizados tomando una ruta de consulta alternativa que elude las restricciones.
Este tipo de vulnerabilidad se produce con facilidad.
Veamos un ejemplo sencillo (con Django y django-graphene):
Tenemos una pequeña plataforma de red social en la que los usuarios pueden conversar entre sí y también publicar posts.
from django.db import models
from django.contrib.auth.models import AbstractUser
class SocialUser(AbstractUser):
pass
class Post(models.Model):
user = models.ForeignKey(SocialUser, related_name='posts', on_delete=models.CASCADE)
content = models.TextField()
class Discussion(models.Model):
user = models.ForeignKey(SocialUser, related_name='discussions', on_delete=models.CASCADE)
content = models.TextField()
Y con `graphene_django`, tenemos los siguientes tipos:
class DiscussionType(DjangoObjectType):
class Meta:
model = models.Discussion
class PostType(DjangoObjectType):
class Meta:
model = models.Post
class UserProfileType(DjangoObjectType):
class Meta:
model = models.SocialUser
Declaramos las siguientes queries:
class Query(graphene.ObjectType):
my_discussions = graphene.List(DiscussionType)
posts = graphene.List(PostType)
my_profile = graphene.Field(UserProfileType)
def resolve_my_discussions(self, info):
user = info.context.user
if user.is_anonymous:
raise Exception("Not logged in!")
return user.discussions.all() # Only the discussions of the logged in user
def resolve_posts(self, info):
user = info.context.user
if user.is_anonymous:
raise Exception("Not logged in!")
return models.Post.objects.all() # All posts are public of course.
def resolve_my_profile(self, info):
user = info.context.user
if user.is_anonymous:
raise Exception("Not logged in!")
return user
A primera vista, no hay nada incorrecto. Un usuario solo puede ver sus propias discusiones

También puede recuperar una lista de posts públicos de otros usuarios.

Pero si observa con más atención el tipo `PostType`, verá que tiene un campo UserProfileType.

Esto se debe a que el modelo Post tiene una `ForeignKey` hacia el modelo SocialUser y django-graphene lo expuso utilizando el primer Object Type que encontró.

Por lo tanto, podemos ejecutar la siguiente consulta para obtener acceso no autorizado a las discusiones de otros usuarios.

Extensiones de GraphQL: el modo de depuración como ejemplo.
¿Qué son las extensiones de GraphQL?
Las extensiones de GraphQL son fragmentos de código que añaden nuevas funciones a su configuración de GraphQL. Ayudan a hacer cosas que GraphQL normalmente no hace por sí mismo, como añadir nuevos ObjectTypes y exponer información de depuración.
Aunque se trata de una función útil, algunas de esas extensiones, que se implementan de forma predeterminada en ciertos servidores GraphQL como Graphene-Django y graphql-ruby, pueden causar problemas de seguridad graves.
Depuración de GraphQL (severidad alta)
Al abordar problemas en GraphQL, los desarrolladores utilizan aplicaciones con información de depuración.
Cuando el modo de depuración está habilitado, un servidor GraphQL proporciona mensajes detallados en respuesta a las solicitudes de los clientes sobre errores del servidor backend que normalmente no se muestran.
Por ejemplo, en lugar de devolver los mensajes de error estándar, un cliente podría recibir un stack trace e información detallada sobre el error.
El modo de depuración de GraphQL está implementado de forma predeterminada en muchas implementaciones de GraphQL, pero no en todas (consulte la tabla siguiente).
Si bien resulta muy valioso durante el desarrollo, dejarlo habilitado en un entorno de producción puede exponer información sensible sobre la estructura interna del servidor y los detalles de su implementación.
Cuando el modo de depuración está habilitado, las respuestas de error pueden incluir:
- Stack traces detallados
- Información de las consultas a la base de datos (incluidas las consultas SQL)
- Rutas internas del servidor y nombres de archivo
- Detalles de configuración sensibles
Ejemplo de una respuesta con el modo de depuración habilitado en Django Graphene, que envuelve la depuración de Django:
{
query {
nonexistentField {
id
}
}
_debug {
sql {
sql
transId
transStatus
isoLevel
encoding
}
}
}
{
"errors":[
{
"message":"Cannot query field \"nonexistentField\" on type \"Query\".",
"locations":[
{
"line":3,
"column":3
}
],
"path":[
"nonexistentField"
]
}
],
"data":null,
"extensions":{
"debug":{
"sql":[
{
"sql":"SELECT \"auth_user\".\"id\", \"auth_user\".\"password\", \"auth_user\".\"last_login\", \"auth_user\".\"is_superuser\", \"auth_user\".\"username\", \"auth_user\".\"first_name\", \"auth_user\".\"last_name\", \"auth_user\".\"email\", \"auth_user\".\"is_staff\", \"auth_user\".\"is_active\", \"auth_user\".\"date_joined\" FROM \"auth_user\" WHERE \"auth_user\".\"id\" = %s LIMIT 21",
"time":"0.000",
"params":[
"1"
]
}
],
"python_version":"3.8.5",
"django_version":"3.1.3",
"graphene_version":"2.1.8",
"graphql_version":"3.0.0"
}
}
}
Impacto en la seguridad del modo de depuración de GraphQL:
-
Divulgación de información: la información de depuración puede revelar detalles internos sobre la estructura del servidor GraphQL, sus dependencias y posibles vulnerabilidades, que los atacantes podrían aprovechar para planificar ataques más dirigidos.
-
Exposición de datos sensibles: los stack traces, los mensajes de error y, en especial, las consultas SQL pueden incluir de forma involuntaria información sensible, como la estructura de la base de datos, rutas internas de archivos o variables de entorno.
-
Explotación más sencilla: los mensajes de error detallados y las consultas SQL pueden ayudar a los atacantes a perfeccionar sus ataques al proporcionarles información inmediata sobre lo que funcionó o no en sus consultas maliciosas.
-
Fuga de información de rendimiento: los atacantes podrían utilizar la información de tiempos que se proporciona sobre las consultas SQL para inferir la estructura de la base de datos o para realizar ataques de temporización.
Para mitigar los riesgos asociados al modo de depuración de GraphQL, lo primero y más importante.
Es fundamental deshabilitar el modo de depuración en producción; por ejemplo, si utiliza Django con Graphene-Django, el modo de depuración en graphql se controla con la misma configuración que la depuración en Django.
settings.py
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
Conclusión
Aunque GraphQL ofrece ventajas significativas para el desarrollo de API, como las consultas flexibles y la obtención eficiente de datos, también introduce desafíos de seguridad propios. Abordar estos desafíos exige un conocimiento profundo no solo de las posibles vulnerabilidades, sino también del servidor GraphQL específico que se utiliza y de las funciones de seguridad que admite.
Esta es una lista de los servidores GraphQL más populares y de las funciones que admiten:
✅ - Habilitado de forma predeterminada
⚠️ - Deshabilitado de forma predeterminada
❌ - Sin compatibilidad

Fuente: graphql-threat-matrix