Segurança em GraphQL: introspection e query depth

1. Introdução aos Riscos de Segurança em GraphQL

GraphQL difere fundamentalmente de REST em termos de segurança. Enquanto REST expõe endpoints fixos e previsíveis, GraphQL oferece um único endpoint que aceita queries arbitrárias definidas pelo cliente. Essa flexibilidade poderosa amplia significativamente a superfície de ataque.

Em REST, um atacante está limitado aos endpoints existentes (/users, /posts). Em GraphQL, qualquer campo definido no schema pode ser consultado em qualquer combinação. Isso significa que dados sensíveis inadvertidamente expostos no schema tornam-se alvos fáceis.

Os principais vetores de ataque em GraphQL incluem:

  • Introspection: exposição completa do schema da API
  • Query Depth: queries profundamente aninhadas que consomem recursos excessivos
  • Query Complexity: queries com muitos campos que sobrecarregam o backend

2. Introspection: O Mapa do Tesouro para Atacantes

Introspection é um recurso nativo do GraphQL que permite consultar o schema completo da API. Em desenvolvimento, é essencial para ferramentas como GraphiQL e GraphQL Playground. Em produção, porém, torna-se um mapa detalhado para atacantes.

Considere este schema hipotético:

type Query {
  user(id: ID!): User
  users: [User]
}

type User {
  id: ID!
  username: String!
  email: String!
  passwordHash: String!  # Campo sensível!
  internalToken: String! # Outro campo sensível
  creditCard: CreditCard
}

Um atacante pode usar introspection para descobrir passwordHash e internalToken:

# Query de introspection
query {
  __schema {
    types {
      name
      fields {
        name
        type {
          name
        }
      }
    }
  }
}

Ferramentas comuns de exploração incluem:

  • GraphiQL: interface interativa para explorar schemas
  • GraphQL Voyager: visualização gráfica do schema
  • Scripts automatizados: ferramentas como graphql-introspection que extraem schemas completos

3. Mitigando Introspection em Produção

A mitigação mais direta é desabilitar introspection em produção. Exemplos práticos:

Apollo Server (Node.js):

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
});

GraphQL-Java:

GraphQLSchema schema = GraphQLSchema.newSchema()
    .query(queryType)
    .build();

GraphQL graphQL = GraphQL.newGraphQL(schema)
    .instrumentation(new MaxQueryDepthInstrumentation(5))
    .build();

// Desabilitar introspection
GraphQL graphQLNoIntrospection = GraphQL.newGraphQL(schema)
    .instrumentation(new DisableIntrospection())
    .build();

Estratégias alternativas:

  • Whitelist de IPs: permitir introspection apenas para IPs internos
  • Autenticação condicional: exigir token específico para acesso ao __schema
  • Ambientes segregados: manter introspection ativo apenas em staging/desenvolvimento

4. Query Depth: Ataques de Complexidade e Negação de Serviço

Query depth refere-se ao nível de aninhamento de uma query. Queries profundas podem causar estouro de recursos no backend, especialmente quando envolvem joins e resoluções de dados complexas.

Exemplo de ataque com query profundamente aninhada:

query DeepQuery {
  user(id: 1) {
    posts {
      comments {
        author {
          posts {
            comments {
              author {
                posts {
                  comments {
                    author {
                      username
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Esta query com 6 níveis de profundidade pode gerar milhares de consultas ao banco de dados. Um atacante pode disparar centenas dessas queries simultaneamente, causando um ataque de negação de serviço (DoS).

O custo de processamento cresce exponencialmente com cada nível adicional de aninhamento, especialmente em schemas com relações N+1.

5. Implementando Limites de Query Depth

Exemplo com graphql-depth-limit no Node.js:

const depthLimit = require('graphql-depth-limit');
const { ApolloServer } = require('apollo-server');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(5)], // Máximo de 5 níveis
});

Exemplo com Python (Ariadne):

from ariadne import graphql_sync
from ariadne.validation import depth_limit_validator

def create_depth_middleware(max_depth=5):
    async def middleware(request, schema, query):
        errors = depth_limit_validator(schema, max_depth)(query)
        if errors:
            return {
                "errors": [{"message": f"Query exceeds maximum depth of {max_depth}"}]
            }
        return None
    return middleware

Tratamento de erros:

# Resposta para cliente legítimo
{
  "errors": [
    {
      "message": "Query depth (8) exceeds maximum allowed depth (5)",
      "locations": [{"line": 2, "column": 3}]
    }
  ]
}

6. Além do Depth: Query Complexity e Persisted Queries

Query Complexity permite limitar por peso de campos, não apenas por profundidade:

const { createComplexityLimitRule } = require('graphql-validation-complexity');

const server = new ApolloServer({
  validationRules: [
    createComplexityLimitRule(1000, {
      onCost: (cost) => console.log(`Query cost: ${cost}`),
    }),
  ],
});

Campos mais custosos (como users que retorna listas grandes) recebem pesos maiores:

type Query {
  user(id: ID!): User @complexity(value: 1)
  users: [User] @complexity(value: 10)  # Mais caro
}

Persisted Queries (APQ): queries pré-aprovadas armazenadas no servidor:

// Registrar query persistida
apolloClient.registerPersistedQuery(
  'query GetUser { user(id: 1) { name email } }',
  'hash123...'
);

// Executar apenas queries persistidas
const server = new ApolloServer({
  persistedQueries: true,
  cache: new MemcachedCache(),
});

APQ reduz drasticamente a superfície de ataque, pois apenas queries conhecidas são aceitas.

7. Monitoramento e Resposta a Incidentes

Logging de queries rejeitadas:

// Middleware de logging
server.applyMiddleware({
  app,
  context: async ({ req }) => {
    const query = req.body.query;
    const depth = calculateDepth(query);

    if (depth > 5) {
      console.warn(`Query rejeitada por depth excessivo: ${depth} níveis`);
      console.warn(`IP: ${req.ip}, Query: ${query.substring(0, 200)}`);

      // Alertar equipe de segurança
      await sendAlert({
        type: 'GRAPHQL_DEPTH_EXCEEDED',
        ip: req.ip,
        depth: depth,
        timestamp: new Date().toISOString()
      });
    }
  }
});

Integração com WAF:

  • Configurar regras para bloquear queries com __schema em produção
  • Rate limiting por IP para queries de introspection
  • Análise de padrões: picos de queries rejeitadas indicam tentativas de exploração

8. Conclusão e Checklist de Segurança

GraphQL oferece poder e flexibilidade, mas exige cuidados específicos de segurança. As três camadas fundamentais são:

  1. Desabilitar introspection em produção — remove o mapa da API
  2. Definir limites de depth e complexity — previne ataques de DoS
  3. Implementar persisted queries — reduz superfície de ataque

Checklist de deploy seguro para GraphQL:

  • [ ] Introspection desabilitado em produção
  • [ ] Limite de query depth configurado (recomendado: 5-7 níveis)
  • [ ] Limite de query complexity configurado
  • [ ] Rate limiting implementado por IP
  • [ ] Logging de queries rejeitadas ativo
  • [ ] Persisted queries consideradas para APIs críticas
  • [ ] Testes de penetração regulares no endpoint GraphQL

A segurança em GraphQL não é opcional — é uma responsabilidade contínua que deve ser integrada ao ciclo de desenvolvimento.

Referências