Uma Visão Ofensiva de Segurança sobre o Método HTTP QUERY
🇬🇧 Read this analysis in English
Se você escreve APIs há quinze anos, já conhece o problema que o QUERY foi inventado para resolver. Em algum momento você precisou enviar uma requisição de leitura com um filtro complexo demais para caber em uma URL, e fez o que todo mundo faz: mandou um POST /search e fingiu que era uma leitura. Funcionou. Também jogou fora, silenciosamente, todas as garantias que o protocolo dá sobre requisições seguras (safe), idempotentes e cacheáveis.
Em junho de 2026 o IETF padronizou uma correção. A RFC 10008, The HTTP QUERY Method (publicada em 15 de junho de 2026) define um método que é seguro e idempotente como o GET, mas carrega um corpo de requisição como o POST, o primeiro método HTTP genuinamente novo desde que o PATCH chegou na RFC 5789, lá em 2010.
A maior parte da cobertura que você viu por aí trata isso como uma vitória de developer experience. E é. Mas este post foi escrito para o outro lado da mesa. Porque as mesmas três propriedades que tornam o QUERY conveniente, um método novo, um corpo de requisição e uma cache key calculada sobre esse corpo, se alinham quase perfeitamente com as três classes de bug mais antigas e mais lucrativas da pilha HTTP. O QUERY não inventa vulnerabilidades novas. Ele reabre vulnerabilidades antigas, todas de uma vez, sobre uma infraestrutura que nunca foi testada contra elas.
Vamos percorrer toda a superfície.
Antes de entrar nos detalhes, aqui está o frame para manter na cabeça durante o resto do post:
O QUERY é a pior combinação possível para quem defende. Ele precisa ser inspecionado como um
POST(corpo arbitrário, CSRF, limites de tamanho) e cacheado como umGET(poisoning, deception), e quase nenhum intermediário no mundo real trata os dois lados ainda.
Tudo abaixo é um desdobramento dessa frase.
Antes do QUERY, você tinha exatamente duas opções, e as duas mentem para alguma parte da pilha.
GET com tudo na URL é semanticamente honesto, safe, idempotente, cacheável, mas a query precisa caber na URL:
GET /users?role=admin&status=active&dept=eng&perm=read&perm=write HTTP/1.1
Isso quebra no momento em que o filtro cresce ou fica profundamente aninhado. URLs têm limites práticos de tamanho (~8 KB na maioria das stacks), estruturas aninhadas codificam mal, e, aqui está a parte que importa mais adiante, a própria RFC observa explicitamente que uma URI tem mais chance de ser logada ou processada por intermediários do que o conteúdo da requisição. Seu filtro acaba parando em access logs, histórico do navegador, headers Referer e favoritos.
POST /search resolve o problema do corpo e mente sobre todo o resto. Nada no protocolo diz a um intermediário que a requisição é só leitura, então caches não cacheiam, proxies assumem efeitos colaterais, e a lógica automática de retry se recusa a repetir a chamada. O preço que você pagou para ganhar esse corpo foi abrir mão de todas as garantias de segurança do protocolo.
O QUERY fecha essa lacuna. Pela RFC 10008 §1, o método é:
- Safe, o cliente não solicita nem espera uma mudança de estado (mesmo contrato semântico do
GET); - Idempotente, pode ser reenviado após uma conexão derrubada sem se preocupar com estado parcial;
- Cacheável, e aqui está a frase que sustenta este post inteiro, a cache key incorpora o conteúdo da requisição.
Essa última propriedade é onde vamos passar a maior parte do tempo. Guarde ela.
Você vai ver posts por aí afirmando “o Node suporta QUERY desde 2024.” Isso é tecnicamente verdade e precisamente enganoso, e a distinção importa porque a maturidade da infraestrutura ao redor é, em si, um sinal de ataque. Quanto mais recentemente um método foi adicionado a um parser, maior a chance de dois hops em uma cadeia de requisição discordarem sobre como tratá-lo, e discordância é a matéria-prima tanto de smuggling quanto de cache poisoning.
Aqui está a linhagem real:
| Data | Marco |
|---|---|
| Nov 2008 | RFC 5323 (WebDAV SEARCH), o ancestral direto: um método safe e idempotente carregando um corpo XML |
| 2019 | Asbjørn Ulsberg reabre a ideia na comunidade HTTP (creditado nos agradecimentos da RFC) |
| Mar 2021 | draft-ietf-httpbis-safe-method-w-body-00, originalmente chamado de “HTTP SEARCH” |
| 25 jan 2024 | nodejs/node issue #51562, a mais antiga demanda concreta de implementação em um runtime mainstream |
| ~Fev 2024 | Node 21.7.2 (via atualização do llhttp) passa a parsear QUERY nativamente; suporte estável chega na linha LTS 22.x |
| 15 jun 2026 | RFC 10008 publicada como Proposed Standard, registrada no IANA HTTP Method Registry |
Então a mais antiga demanda pública de implementação é a issue do Node de janeiro de 2024, aberta apontando para a draft-02. Mas a ideia é descendente direta do SEARCH do WebDAV, de 2008. O que fica para quem testa: a versão do Node do alvo diz se o runtime sequer entende o método, e “o runtime faz parse” não é o mesmo que “o framework roteia” — diferença que ficou clara na prática, e não sem dor, na issue do Express que rastreia o suporte a QUERY: o Node já entendia o método, mas o framework por cima dele não sabia rotear as requisições corretamente.
Se você quer um modelo mental: você está testando um método que é, ao mesmo tempo, um conceito com dezessete anos e código de produção com dezoito meses. É nessa lacuna que os bugs vivem.
A combinação método novo + corpo + cache toca as três classes de bug de camada HTTP mais lucrativas da história de bug bounty. As próximas três partes ganham cada uma seu mergulho profundo, mas aqui está o mapa:
- Request smuggling / desync, um método novo que metade da cadeia interpreta de forma diferente → desacordo entre front end e back end.
- Cache poisoning / deception, a cache key agora inclui um corpo controlado pelo atacante e sem limite → normalização inconsistente.
- CSRF / a armadilha do “safe”, “safe” na spec não significa “sem efeitos colaterais” na implementação.
Antes de mergulhar, o crédito onde ele é devido, porque você deveria saber em cima de qual pesquisa está pisando, e porque a história do QUERY é uma história de reincidência, não de território novo.
- Amit Klein, hoje professor na Hebrew University of Jerusalem; ex VP of Security Research na SafeBreach e CTO da Trusteer. Ele escreveu a pesquisa original de web cache poisoning em 2004 e coescreveu o paper original de request smuggling em 2005.
- Chaim Linhart, Amit Klein, Ronen Heled e Steve Orrin, o time da Watchfire (depois adquirida pela IBM) que escreveu o paper seminal de 2005.
- Régis “Regilero” Leroy, reacendeu a técnica entre 2015 e 2016 (“Hiding Wookiees in HTTP,” DEF CON 24), durante a década em que ela ficou dormente.
- James Kettle (
albinowax), Director of Research na PortSwigger, criadora do Burp Suite. Ele repopularizou o smuggling em escala moderna em 2019 e empurrou o cache poisoning para frente. Apresenta na Black Hat há uma década. - Omer Gil, cunhou o termo “Web Cache Deception” em 2017.
- sw33tLie, bsysop, Medusa e Jeppe Weikop, bug hunters full time por trás das primitivas de desync mais recentes (TE.0 em 2024, TE.TE em 2025), depois consolidadas por Kettle.
Muito do que se escreve recentemente credita quase tudo isso ao Kettle. Isso é impreciso, e se você citar dessa forma em um report vai perder credibilidade com quem conhece a área. A atribuição honesta:
| Técnica | Origem real | Papel do Kettle |
|---|---|---|
| Request smuggling | Linhart, Klein, Heled, Orrin, Watchfire, 2005 | Reviveu em escala moderna (2019) |
| Web cache poisoning | Amit Klein, “Divide and Conquer” (2004) | Practical Web Cache Poisoning (2018), Web Cache Entanglement (2020) |
| Web cache deception | Omer Gil (2017) | Independente do Kettle |
| Revival do smuggling | Regilero (2015 a 2016), depois Kettle (2019) | Deu escala e ferramentas (Burp) |
| TE.0 dechunking | sw33tLie / bsysop / Medusa (2024) | N/A |
| TE.TE / 0.CL | Jeppe Weikop / Kettle (2025) | Consolidado em “HTTP/1.1 Must Die” |
A ponte para o QUERY: o método não cria nenhuma dessas classes. Ele abre um eixo novo, um método desconhecido carregando um corpo que é cacheado, onde as três podem ressurgir de uma vez só.
Request smuggling, hoje conhecido como desync, foi documentado pela primeira vez pelo time da Watchfire em 2005. O mecanismo não mudou desde então:
Uma requisição HTTP passa por uma cadeia de servidores, front end (CDN / proxy / WAF) e depois back end (origin). Cada hop precisa saber onde uma requisição termina e a próxima começa. No HTTP/1.1 esse limite é definido por dois headers de framing: Content-Length e Transfer-Encoding. Quando dois hops discordam sobre esse limite, um atacante pode esconder uma segunda requisição que só um deles enxerga. Essa requisição contrabandeada pula as regras do front end (bypass de WAF) e se anexa na frente da requisição do próximo usuário.
Se você quer o tratamento moderno canônico, HTTP Desync Attacks: Request Smuggling Reborn (2019), do Kettle, é o paper que repopularizou a classe, e HTTP/1.1 Must Die (2025) é o estado da arte atual, reportando vetores de desync afetando dezenas de milhões de sites e rendendo cerca de US$ 350.000 em bounties, incluindo o CVE-2025-32094 contra a Akamai.
Smuggling precisa de duas coisas: (1) um desacordo de parsing entre hops, e (2) um lugar para esconder a requisição contrabandeada. O QUERY entrega os dois.
O desacordo é quase garantido. O QUERY é um método novo. Uma CDN moderna encaminha ele; um WAF ou load balancer legado pode rejeitar, ignorar ou, o mais útil de tudo, tratar de forma diferente da origem. Como uma análise da superfície de ataque da RFC coloca, políticas antigas de WAF, proxy, framework e load balancer podem ainda não reconhecer o QUERY; algumas stacks rejeitam, outras roteiam ou inspecionam de forma diferente do POST. Cada uma dessas inconsistências é uma costura explorável.
O esconderijo já vem embutido. Diferente do GET, o QUERY tem um corpo legítimo. Historicamente, servidores que aceitavam corpo em um método sem corpo (“fat GET”) eram um vetor de smuggling e cache poisoning, e o conselho de longa data do Kettle era simplesmente não suporte fat GET. O QUERY padroniza exatamente essa forma, um método portador de corpo, cacheável e safe, mas agora com uma spec que exige que o corpo seja lido. O antipadrão virou o padrão.
E toda a família CL.TE / TE.CL / TE.0 / TE.TE, as quatro “interpretações de comprimento” que Kettle enumera no paper de 2025 (CL, TE, zero implícito 0, e o H2 do HTTP/2), se aplica diretamente, só que agora contra parsers que talvez nunca tenham sido fuzzados com um verbo QUERY.
Aqui está a parte que vale a pena deixar precisa, porque é fácil imaginar o ataque errado. O smuggling não acontece “dentro” do corpo JSON do QUERY. Ele acontece uma camada acima do corpo, no framing da mensagem HTTP: os headers Content-Length / Transfer-Encoding e o nome do método desconhecido.
Divisão de trabalho:
- O método desconhecido confunde o parser (esse hop roteia um QUERY? rejeita? cai para um fallback?).
- O corpo do QUERY é o esconderijo para a requisição contrabandeada.
- O JSON em si é só a fachada que faz a requisição externa parecer uma busca inocente.
Há um detalhe revelador na spec aqui. As Security Considerations da RFC 10008 nunca usam a palavra “smuggling,” nem uma vez. Isso não é descuido, smuggling é uma falha de implementação de intermediário, não da semântica do método, então está genuinamente fora do escopo da RFC. Mas do ponto de vista ofensivo, uma lacuna não endereçada em um método recém-nascido é exatamente onde você quer estar olhando.
Um desync QUERY.TE, o front end respeita Content-Length, o back end respeita Transfer-Encoding:
QUERY /search HTTP/1.1
Host: target.example
Content-Type: application/json
Content-Length: 118
Transfer-Encoding: chunked
5c
{"q":"laptop","filters":{"price":{"lt":500}}}
0
QUERY /admin/users HTTP/1.1
Host: target.example
X-Ignore: x
O que acontece, passo a passo:
- Dois headers de framing entram em conflito.
Content-Lengthdiz 118 bytes;Transfer-Encodingdiz “leia em chunks.” A RFC 9112 §6.3 exige queTransfer-Encodingvença e que oContent-Lengthseja descartado, mas nem todo hop obedece. - O front end respeita
Content-Length. Um WAF/proxy legado lê 118 bytes, encaminha tudo como uma requisição QUERY, e deixa passar, parece uma busca comum. - O back end respeita chunked. A origem lê o chunk
5c, atinge o0de término, e trata o restante como uma segunda requisição:QUERY /admin/users. - Resultado: bypass de WAF. A segunda requisição nunca passou pelas regras do front end. O “corpo de busca” foi o esconderijo da requisição contrabandeada.
A viabilidade depende do par proxy↔origem específico, então teste o front end e a origem isoladamente e compare como cada um interpreta a mensagem. A correção estrutural é a que Kettle vem defendendo desde 2025: HTTP/2 ponta a ponta, incluindo o upstream. Vale notar que a maioria das stacks termina o HTTP/2 na borda e faz downgrade para HTTP/1.1 no upstream, o que, como o paper aponta, é ainda mais perigoso do que HTTP/1.1 ponta a ponta porque introduz uma quarta interpretação de comprimento. É nesse downgrade que o desync baseado em QUERY vai morar.
Se o smuggling é o clássico reciclado, o cache poisoning é o risco estrutural e novo do QUERY, porque a RFC move deliberadamente a cache key para dentro do corpo.
Cache poisoning foi descrito pela primeira vez por Amit Klein em 2004 (“Divide and Conquer: HTTP Response Splitting, Web Cache Poisoning Attacks, and Related Topics”) e aprofundado por Kettle em Practical Web Cache Poisoning (2018) e Web Cache Entanglement (2020).
Um cache armazena respostas para que a origem não seja acionada toda vez. Para achar a resposta certa, ele calcula uma cache key. Tradicionalmente essa key é URL + alguns headers. Duas requisições com a mesma key recebem a mesma resposta armazenada. Poisoning é a arte de armazenar uma resposta maliciosa sob uma key que uma vítima também vai gerar, fazendo com que a vítima receba o conteúdo do atacante.
As lições centrais do Kettle no Entanglement valem ser citadas em espírito: componentes que parecem seguros por estarem dentro da cache key se tornam perigosos quando são parseados, transformados e normalizados de forma inconsistente entre o cache e a aplicação. Seus dois conselhos permanentes: reescreva a requisição, não a cache key, e não suporte fat GET.
Leia a RFC 10008 §2.7 e você vai ver a spec fazer, por design, exatamente aquilo contra o qual o Kettle passou anos alertando:
- A cache key incorpora o conteúdo da requisição, controlado pelo atacante, efetivamente sem limite.
- Para melhorar a eficiência, a RFC explicitamente permite que caches normalizem o corpo antes de gerar a key: “caches MAY remove semantically insignificant differences from request content,” por exemplo removendo content encodings ou “normalizing based upon knowledge of format conventions, as indicated by any media subtype suffix in the request’s
Content-Typefield (e.g.,+json).”
E a RFC é honesta sobre a consequência. Em suas próprias palavras, se um cache aplica um modelo de normalização diferente do da origem, duas queries distintas podem ser tratadas como equivalentes, fazendo com que a resposta errada seja retornada. Isso é uma primitiva de poisoning descrita na própria especificação. Como uma análise da spec observou, em sistemas multi-tenant esse tipo de erro se torna uma vulnerabilidade séria.
Toda normalização permitida é uma discrepância em potencial. É o risco do “fat GET,” agora padronizado e espalhado por um espaço muito maior e mais estruturado do que os headers jamais ofereceram.
O vetor inicial mais provável é a normalização divergente de JSON. Considere uma chave duplicada:
Requisição do atacante:
QUERY /products HTTP/1.1
Host: shop.example
Content-Type: application/json
{"q":"tv", "q":"<script>evil()</script>"}
Requisição da vítima:
QUERY /products HTTP/1.1
Host: shop.example
Content-Type: application/json
{"q":"tv"}
A cadeia de eventos:
- Chave duplicada de propósito. O JSON contém
qduas vezes. Parsers de JSON discordam sobre chaves duplicadas, alguns pegam o primeiro valor, outros o último. Se o cache e a origem resolvem isso de formas diferentes, você tem sua discrepância. - O cache gera a key pelo primeiro
q. O cache normaliza paraq=tv, a mesma key que a vítima vai gerar. Para o cache, as duas requisições são idênticas. - A origem processa o segundo
q. A origem executa com o payload malicioso e retorna uma resposta contaminada, que o cache armazena sob a keytv. - A vítima recebe o payload. Qualquer um que buscar
tvrecebe a resposta envenenada do cache. Uma requisição, impacto em todo mundo.
Além de chaves duplicadas, as variações que valem a pena testar são exatamente as “diferenças semanticamente insignificantes” que a RFC convida os caches a colapsar: reordenação de campos, espaços em branco, Unicode ambíguo e formas numéricas (1 vs 1.0 vs 1e0). O bug existe sempre que o cache e a origem discordam sobre o que conta como “o mesmo corpo.”
A mesma propriedade “corpo = key, sem limite de tamanho” abre mais duas portas, ambas levantadas por pesquisadores na discussão pública em torno da RFC:
Cache busting / flooding. A key não tem limite e é controlada pelo atacante. Varie um único byte e você força um miss:
QUERY /search → {"q":"a","_":"rnd-0001"}
QUERY /search → {"q":"a","_":"rnd-0002"}
QUERY /search → {"q":"a","_":"rnd-0003"} ...
Ou toda requisição erra o cache e a carga total volta para a origem (uma amplificação de custo, um DoS), ou você inunda o cache com milhões de entradas únicas e expulsa as legítimas.
Cache deception, o ataque irmão que Omer Gil divulgou em 2017 contra o PayPal (também apresentado na Black Hat USA 2017). Aqui você engana o cache para armazenar uma resposta sensível e personalizada sob uma key que o atacante também consegue gerar, e depois lê os dados cacheados da vítima. Com o QUERY isso piora via o mecanismo Location / Content-Location: a RFC 10008 §2.4 permite que uma resposta de QUERY aponte para um recurso equivalente acessível via GET, e se essa URI temporária é previsível ou vaza a query, ela vira um alvo direto.
Se você testa GraphQL, esta seção é onde o risco abstrato vira concreto para um ecossistema real, com uma ressalva que vale deixar clara desde já: isso é um futuro candidato, não algo já em produção.
O GraphQL sempre usou POST para leituras, porque queries não cabem em uma URL, o que significava abrir mão do cache HTTP. A spec de GraphQL over HTTP já suportava GET mais persisted queries como um workaround parcial, e no papel o QUERY resolve o problema de forma limpa: um corpo completo e cacheabilidade nativa. Mas “resolve de forma limpa no papel” não é o mesmo que “adotado,” então vale ser preciso sobre onde as coisas realmente estão.
Eu busquei a draft atual da spec de GraphQL over HTTP diretamente ao escrever este post, e ela não menciona o método QUERY em nenhum lugar. GraphQL sobre QUERY também não faz parte da RFC 10008: a RFC define o método, não os formatos de payload que rodam sobre ele, e se e como os clientes GraphQL vão adotar o QUERY é uma discussão separada e ainda em aberto.
O que existe hoje é entusiasmo, não compromisso. Quando a RFC 10008 bombou no Hacker News e gerou mais de cem comentários, o GraphQL surgiu quase imediatamente, vários comentaristas apontaram que o QUERY é um encaixe semântico natural para queries GraphQL (em oposição às mutations, que não são safe). Isso é a comunidade concordando que o encaixe faz sentido, não um roadmap comprometido pela GraphQL Foundation.
O sinal de engenharia mais concreto até agora, na verdade, vem de fora do próprio GraphQL. O pull request do Spring Framework adicionando suporte à RFC 10008 esbarrou direto no fato de que @QueryMapping já existe como uma annotation no Spring GraphQL. A resposta do mantenedor Brian Clozel: “We will do a thorough review of this PR and synchronize with other Spring projects… there are many aspects to consider (like the fact that @QueryMapping already exists in spring graphql) but we would like to support this in time for Spring Framework 7.1 in November.” Uma issue complementar rastreia o suporte ao RequestMethod central separadamente. Isso é um time real fazendo trabalho real de coordenação, mas é encanamento para o método HTTP em geral, não um anúncio de GraphQL sobre QUERY.
Como uma análise da RFC colocou sem rodeios: “the current GraphQL over HTTP draft has not adopted QUERY; using it in practice means waiting for GraphQL servers, clients, CDNs, and frameworks to support it,” e a adoção ao longo de toda essa cadeia, servidores, CDNs, reverse proxies e navegadores, é honestamente “a multi year road,” não algo para o próximo trimestre.
Então trate o que vem a seguir como condicional. O risco ainda não existe porque o transporte ainda não existe para o GraphQL especificamente. Ele passa a existir no dia em que um servidor, framework ou gateway GraphQL começar a aceitar QUERY, e dado o encaixe semântico e o precedente do Spring, esse dia é uma questão de quando, não de se. É exatamente por isso que isso pertence a um checklist de recon agora, e não depois que já estiver rodando em todo lugar.
Mas tornar o GraphQL cacheável na borda reativaria riscos que o POST não cacheável hoje mascara, acidentalmente.
Primeiro, o termo que vale a pena definir com precisão. Um ataque de batching explora a capacidade do GraphQL de enviar múltiplas operações em uma única requisição HTTP. Um rate limiter que conta “requisições por segundo” conta uma, mas a origem executa N operações. O artigo da Checkmarx mostra o abuso canônico: 100 tentativas de login em uma única requisição, passando reto por um rate limit por requisição:
[ {"query":"mutation{login(u:\"joe\",p:\"pass1\"){token}}"},
{"query":"mutation{login(u:\"joe\",p:\"pass2\"){token}}"},
... x100 ]
Envolva isso em um QUERY que o WAF trata como uma leitura safe e cacheável, e fica ainda mais silencioso. O OWASP GraphQL Cheat Sheet documenta abusos de batching, depth e alias com mais profundidade.
Os riscos que se somam especificamente com o QUERY:
- Poisoning de uma query cacheada, normalização divergente do corpo GraphQL serve a resposta de um usuário para outro. Este é o vetor da Parte 5, mirado no GraphQL.
- Bombas de complexidade / profundidade, uma query profundamente aninhada que parece barata mas é cara para resolver. Se é cacheável e a key varia, cada requisição paga o custo total, um DoS de custo.
- Introspection + cache, um schema exposto acelera o recon, e um cache mal-chaveado permite reutilizar ou enumerar respostas.
Esta é a fronteira mais nova, e a menos explorada, então vale a pena marcar posição cedo.
Ferramentas de agentes, o lado de retrieval de protocolos como o MCP e mensageria agente a agente, que buscam dados são, semanticamente, queries, não mutations. Até agora o transporte não conseguia dizer isso: uma leitura e uma escrita vindas de um agente pareciam idênticas para tudo entre o agente e os dados. O QUERY dá ao retrieval seu próprio método, safe, idempotente e cacheável, o que é genuinamente útil, e genuinamente uma superfície de ataque nova.
Por que agentes amplificam o risco. Eles fazem retry automaticamente (idempotência convida a isso), e enviam corpos grandes e estruturados, embeddings, filtros de metadados, parâmetros de reranking. Isso maximiza tanto o benefício do cache quanto a superfície de colisão.
A implicação ofensiva principal: context poisoning via cache HTTP. Se a camada de retrieval (RAG) de um agente fala QUERY cacheável, envenenar essa entrada de cache injeta dados falsos direto no contexto do modelo, sem tocar no prompt nem no modelo. O cache HTTP vira um vetor de manipulação para o agente.
Um esboço da PoC:
QUERY /rag/retrieve HTTP/1.1
Host: agent-backend.example
Content-Type: application/json
{"embedding":[0.12,0.98,...],"topk":5,"filter":{"doc":"policy"}}
- O atacante descobre o comportamento da cache key desse endpoint (o corpo é normalizado?) e força uma colisão com a query legítima do agente.
- Sob essa key, ele semeia um “documento recuperado” contendo fatos falsos ou instruções injetadas.
- Na próxima recuperação idêntica, o agente recebe o conteúdo envenenado do cache e trata como contexto confiável.
Se você faz trabalho ofensivo adjacente a sistemas de LLM, é aqui que a história do QUERY para de ser um rehash de 2005 e vira algo novo.
O erro de desenvolvedor mais provável, e um achado fácil em um alvo real.
“Safe,” pela RFC 9110 §9.2.1 e reafirmado na RFC 10008, significa apenas que o cliente não solicita uma mudança de estado, não que a implementação não tem efeitos colaterais. São afirmações bem diferentes.
Endpoints de busca frequentemente têm efeitos colaterais escondidos: escrever em uma tabela de “buscas recentes,” atualizar um timestamp de last_seen que alimenta rate limiting, incrementar um contador, ou disparar analytics com consequências reais no downstream:
QUERY /search HTTP/1.1
{"q":"laptop"}
| efeitos colaterais não óbvios:
+-> INSERT INTO recent_searches(...)
+-> UPDATE user SET last_seen = now()
Se um middleware de CSRF só protege a lista clássica (POST / PUT / DELETE / PATCH), um endpoint QUERY com efeito colateral passa direto, a mesma classe do CSRF baseado em GET, mas em um método que ninguém pensou em auditar ainda.
A mitigação parcial da spec, e seus limites. As Security Considerations da RFC 10008 observam que o QUERY não está na safelist de CORS do WHATWG Fetch, então JavaScript no navegador precisa enviar um preflight OPTIONS, o mesmo que PUT ou DELETE. Isso fecha o ataque ingênuo de “contrabandear um corpo cross origin através do CORS porque o método parece um GET” em navegadores conformes. Mas isso não cobre requisições same origin, clientes que não são navegadores, chamadas servidor a servidor, ou servidores que respondem ao preflight de forma permissiva (Access-Control-Allow-Methods: ..., QUERY). E como uma análise reforça, o risco prático não está na spec, está em saber se todo hop na sua cadeia realmente trata o preflight e o QUERY subsequente de forma consistente.
Uma observação estrutural antes do checklist. A decisão “isso é cacheável e safe” não é tomada pela aplicação, é tomada na borda (CDN, reverse proxy, WAF). E não é coincidência que duas das maiores CDNs escreveram o padrão.
Os autores da RFC 10008 são Julian Reschke (greenbytes), James M. Snell (Cloudflare) e Mike Bishop (Akamai), o mesmo J. M. Snell que, dezesseis anos antes, coescreveu a RFC 5789 (PATCH). Cacheabilidade na borda é o core business de uma CDN; as partes que mais lucram com “buscas complexas voltarem a ser cacheáveis” também são as que carregam o peso de calcular corretamente essa cache key inclusiva do corpo.
Há um precedente útil aqui. A Cloudflare já “hackeou” o caching de POST via a Workers Cache API: calcular um SHA-256 do corpo, transformá-lo em uma key sintética de GET. É exatamente isso que o QUERY padroniza, o que significa que os bugs desse padrão manual (hash calculado errado, corpo não lido por completo, normalização divergindo da origem) migram para a implementação nativa. Minha hipótese de trabalho sobre onde caçar primeiro: configurações customizadas de Workers / cache key onde um desenvolvedor normaliza o corpo do QUERY de forma diferente da origem. E como o suporte no nível de CDN tende a ser lançado antes do suporte em frameworks, essa lacuna borda↔origem, o terreno clássico do desync, vai ficar escancarada durante a transição.
Um plano de teste que você pode colar direto em um escopo de engagement.
Recon / descoberta
- Faça
OPTIONSno endpoint → verifique oAllow:procurando porQUERY. - Procure pelo header de resposta
Accept-Query(anuncia suporte e os media types aceitos). - Envie um
QUERYàs cegas; um405 + Allowou415 + Accept-Queryconfirma ou nega o suporte. - Mapeie endpoints
POSTque na verdade são leituras:/search,/filter,/report,/graphql.
Smuggling / desync
- Compare um
QUERYenviado direto para a origem vs. através da CDN. QUERY+Transfer-Encoding: chunked(e chunk extensions, no estilo TE.TE).Content-LengtheTransfer-Encodingconflitantes.- Downgrade de HTTP/2 para 1.1 no hop upstream.
Cache poisoning / deception
- Encontre um oráculo de cache (hit/miss detectável).
- JSON: chaves duplicadas, reordenação, espaços em branco, Unicode ambíguo, formas numéricas.
- Cache busting: loop de corpos únicos, meça a carga na origem.
- Inspecione
Location/Content-Locationem busca de URIs previsíveis ou que vazem dados.
CSRF, IA e agentes
- Endpoints QUERY com efeitos colaterais observáveis não cobertos por CSRF.
- Preflight permissivo? Caminhos same origin e não-navegador.
- Endpoints de RAG / GraphQL migrados para QUERY.
- Envenene um cache de retrieval → context poisoning.
A metade defensiva, para fechar o ciclo do seu write up:
- Allowlist explícita de métodos no WAF / gateway / LB, QUERY tratado deliberadamente, nunca por fallback acidental.
- Escrutínio de corpo no nível do POST, as mesmas regras de SQLi/XSS/injection e limites de tamanho aplicados ao corpo do QUERY.
- Cache key correta, faça hash do corpo completo; normalize de forma idêntica entre cache e origem, ou não normalize nada. O mantra do Kettle: reescreva a requisição, não a cache key.
- Cobertura de CSRF para qualquer endpoint QUERY com efeito colateral, independentemente da designação “safe.”
- HTTP/2 ponta a ponta, incluindo o upstream, a única correção estrutural para a ambiguidade do limite de requisição.
- Preflight restritivo, não adicione QUERY ao
Allow-Methodssem necessidade; mantenha dados sensíveis fora de URIsLocationtemporárias.
O QUERY é uma adição genuinamente boa ao HTTP. POST /search sempre foi uma mentira, e o método finalmente confere com o significado. Mas “bom para a web” e “seguro por padrão” são afirmações diferentes, e é na lacuna entre elas que você trabalha.
Três coisas para levar para o seu próximo engagement:
- O corpo é o novo campo de batalha. Smuggling usa o corpo do QUERY como esconderijo e o framing do HTTP como a ambiguidade. Cache poisoning usa o corpo como uma key envenenável.
- A janela de transição é o momento. Adoção fragmentada, CDN sim, framework não, significa desacordo máximo entre hops. É quando os bugs aparecem.
- Priorize IA e a borda agora, e continue de olho no GraphQL. Endpoints de IA e retrieval RAG são onde os primeiros deployments reais de QUERY estão nascendo, é ali que o impacto mais novo, context poisoning, ainda não tem defesas estabelecidas. GraphQL ainda é um candidato, não um alvo já em produção, mas é o próximo natural a observar dado o encaixe semântico e a coordenação inicial já em curso nos frameworks.
A RFC não inventou um ataque novo. Ela reabriu três antigos, padronizou o antipadrão que os alimenta, e enviou isso para uma infraestrutura que nunca foi testada contra eles. Vá encontrá-los, em sistemas que você está autorizado a testar.
Especificações
- RFC 10008, The HTTP QUERY Method (Reschke, Snell, Bishop; junho de 2026). Ver §2.4 (Location), §2.7 (Caching), §3 (Accept-Query), §4 (Security Considerations).
draft-ietf-httpbis-safe-method-w-body, o histórico da draft do working group (originalmente “HTTP SEARCH,” 2021).- RFC 5323, WebDAV SEARCH (2008), o ancestral do método.
- RFC 5789, PATCH Method for HTTP (Dusseault, Snell; 2010).
- RFC 9110, HTTP Semantics e RFC 9112, HTTP/1.1.
Request smuggling
- Linhart, Klein, Heled, Orrin, HTTP Request Smuggling (Watchfire, 2005).
- Amit Klein, HTTP Request Smuggling in 2020: New Variants, New Defenses and New Challenges (SafeBreach, Black Hat USA 2020).
- James Kettle, HTTP Desync Attacks: Request Smuggling Reborn (PortSwigger, Black Hat / DEF CON 2019).
- James Kettle, HTTP/1.1 Must Die: The Desync Endgame (PortSwigger, Black Hat / DEF CON 2025).
Cache poisoning & deception
- Amit Klein, Divide and Conquer: HTTP Response Splitting, Web Cache Poisoning Attacks, and Related Topics (2004).
- James Kettle, Practical Web Cache Poisoning (2018) e Web Cache Entanglement: Novel Pathways to Poisoning (Black Hat USA 2020).
- Omer Gil, Web Cache Deception Attack (2017) e o white paper da Black Hat USA 2017.
Ecossistema & análises
nodejs/node#51562, a demanda de implementação registrada mais antiga (jan 2024).- Documentação GraphQL Performance e Security; OWASP GraphQL Cheat Sheet; Checkmarx, GraphQL batching attack.
- Draft da spec de GraphQL over HTTP, atual no momento da escrita; não menciona o método QUERY.
- Spring Framework PR #34993, adiciona suporte central à RFC 10008, revela o conflito de nomenclatura
@QueryMappingcom o Spring GraphQL; e a issue de rastreamento #36988 do Spring Framework. - Discussão no Hacker News sobre a RFC 10008, onde o encaixe do GraphQL com o QUERY foi levantado quase imediatamente por comentaristas.
- laioutr, RFC 10008: What the New HTTP Method Means for Frontends, fonte do framing “multi year road” sobre a adoção entre ecossistemas.
- Cloudflare Workers Cache API.
- Model Context Protocol.
Esta é uma pesquisa defensiva/ofensiva para propósitos legítimos. Teste apenas sistemas para os quais você está explicitamente autorizado a avaliar, um programa de bug bounty dentro do escopo ou um engagement contratado.