GraphQL é uma linguagem de consulta e manipulação de dados para APIs que oferece grande flexibilidade ao cliente. No entanto, essa flexibilidade introduz riscos de segurança específicos que não existem em APIs REST tradicionais. Nesta aula, exploraremos quatro áreas críticas de segurança em GraphQL: queries complexas que podem sobrecarregar o servidor, problemas de batching que podem contornar rate limiting, falhas de autorização que expõem dados sensíveis e a exposição indevida de informações através do schema introspection.

Queries complexas

Uma das maiores vantagens do GraphQL é permitir que o cliente solicite exatamente os dados que precisa, mas isso também permite que um atacante construa consultas profundamente aninhadas e com muitos campos, que podem consumir recursos excessivos do servidor. Esse tipo de ataque é conhecido como query complexity attack e pode levar a negação de serviço (DoS).

Para mitigar, é essencial limitar a profundidade máxima das queries, o número de campos e o número de objetos retornados. Ferramentas como graphql-depth-limit e graphql-query-complexity ajudam a calcular e limitar a complexidade. Exemplo de configuração em Node.js:

const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-query-complexity');

const server = new ApolloServer({
  schema,
  validationRules: [
    depthLimit(10),
    createComplexityLimitRule(1000)
  ]
});

Batching

Batching é uma técnica onde múltiplas requisições são agrupadas em uma única consulta GraphQL usando aliases ou campos com argumentos diferentes. Embora legítimo para reduzir overhead de rede, o batching pode ser usado para contornar limites de taxa (rate limiting) baseados em requisições HTTP. Um atacante pode enviar uma única requisição HTTP contendo centenas de operações, cada uma consumindo recursos.

A solução é implementar rate limiting no nível da operação (query) em vez de apenas no nível HTTP. Bibliotecas como graphql-rate-limit ou graphql-request-rate-limit permitem limitar o número de campos ou a complexidade por operação. Além disso, desabilitar o batching automático quando não necessário é uma boa prática. Exemplo de configuração com Apollo Server:

const { ApolloServer } = require('apollo-server');
const { applyMiddleware } = require('graphql-middleware');
const { rateLimit } = require('graphql-rate-limit');

const schema = applyMiddleware(
  makeExecutableSchema({ typeDefs, resolvers }),
  rateLimit({ window: '1m', max: 100 })
);

Autorização

Autorização em GraphQL deve ser aplicada em nível de campo ou resolver, e não apenas no endpoint. Muitas implementações falham ao verificar permissões apenas na raiz da query, permitindo que um usuário não autorizado acesse campos aninhados de objetos que não deveria ver.

A abordagem recomendada é usar um middleware que verifique permissões para cada campo solicitado. Por exemplo, no Apollo Server, pode-se usar graphql-shield para definir regras granulares. Exemplo:

const { shield, rule, and, or, not } = require('graphql-shield');

const isAuthenticated = rule()(async (parent, args, ctx) => {
  return ctx.user !== null;
});

const permissions = shield({
  Query: {
    user: isAuthenticated,
  },
  User: {
    email: isAuthenticated,
  }
});

Nunca confie apenas na falta de exposição do campo no schema; sempre valide a autorização no resolver.

Exposição indevida

Por padrão, GraphQL expõe o esquema completo através de queries de introspection (como __schema e __type). Isso é útil durante o desenvolvimento, mas em produção pode revelar detalhes da API, incluindo campos e argumentos não documentados, facilitando ataques.

Desabilitar a introspecção em produção é uma prática recomendada. No Apollo Server, pode-se fazer assim:

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

Além disso, evite expor mensagens de erro detalhadas que incluam stack traces ou nomes de campos internos. Use um formato de erro genérico e registre detalhes internamente.

Boas práticas

Além das mitigações específicas, adote as seguintes práticas gerais: (1) sempre validar entradas do usuário, (2) usar listas de permissões (allowlist) para operações permitidas em vez de blocklists, (3) implementar logging e monitoramento de consultas anômalas, (4) manter o servidor GraphQL atualizado com as últimas correções de segurança.

Exercícios

  1. Explique como uma query complexa pode causar negação de serviço e cite duas formas de mitigação.
  2. ✓ Resposta: Queries complexas aninhadas podem consumir muitos recursos do servidor, como CPU e banco de dados, levando a DoS. Mitigações: limitar profundidade máxima (ex: 10 níveis) e usar análise de complexidade para rejeitar consultas acima de um limite.
  3. Qual a diferença entre rate limiting baseado em HTTP e rate limiting baseado em operação GraphQL? Por que o segundo é necessário?
  4. ✓ Resposta: Rate limiting HTTP conta requisições HTTP, mas uma única requisição GraphQL pode conter várias operações. O rate limiting por operação limita a quantidade de trabalho por requisição, evitando que batching contorne o limite.
  5. Como garantir que um usuário não autorizado não acesse o campo 'email' de um usuário em GraphQL?
  6. ✓ Resposta: Adicionar uma regra de autorização no resolver do campo 'email' que verifica se o usuário logado é o próprio ou tem permissão. Exemplo com graphql-shield: uma regra que retorna erro se não autenticado.
  7. O que é introspecção no GraphQL e por que deve ser desabilitada em produção?
  8. ✓ Resposta: Introspecção permite consultar o esquema GraphQL (tipos, campos, argumentos). Em produção, isso expõe toda a estrutura da API, auxiliando atacantes. Deve ser desabilitada para evitar vazamento de informações.
  9. Cite duas maneiras de evitar exposição indevida de informações em erros GraphQL.
  10. ✓ Resposta: 1) Formatar erros para exibir apenas mensagens genéricas (ex: "Erro interno") e registrar detalhes no servidor. 2) Não incluir stack traces ou informações de banco de dados nas respostas de erro.

Referências