Em resumo: A entrada da sua API é uma superfície de ataque, então teste-a como tal. Escreva casos negativos que enviem campos superdimensionados, tipos incorretos, corpos malformados e strings de injeção, então verifique se o endpoint responde com um 4xx e nunca com um 5xx. Transforme a validação de esquema em um controle de segurança com `additionalProperties: false`, enums e limites de comprimento. Execute todo o conjunto de testes no CI a cada mudança. Agentes de IA tornam isso urgente: eles geram e encaminham payloads na velocidade da máquina, então “carregar estes dados” silenciosamente se tornando “executar este código” agora escala.
A maioria dos conjuntos de testes prova que sua API funciona quando o chamador é educado. Você envia um corpo válido, recebe um 200, a asserção passa. Esse resultado diz quase nada sobre o que acontece quando o corpo é hostil. Entrada não confiável é qualquer dado que seu endpoint não gerou por si mesmo: corpos de requisição, query strings, cabeçalhos, uploads de arquivos, payloads de webhook e o JSON que um agente de IA monta em tempo real. Tudo isso merece a mesma suposição, que é a de que alguém eventualmente enviará a pior versão possível disso.
Em julho de 2026, o Hugging Face descreveu um incidente de segurança cujo vetor de entrada era dados, não uma senha roubada. Abordamos as lições dessa violação separadamente; este guia é a parte prática. Você construirá testes que enviam o tipo de entrada que um atacante envia e os executará automaticamente a cada mudança. As categorias se alinham com o Top 10 de Segurança de API OWASP, que vale a pena manter aberto em uma aba. O Apidog é uma maneira de projetar o contrato e conduzir esses testes, mas as ideias se aplicam a qualquer framework que você já utilize.
A entrada é uma superfície de ataque, não um campo de formulário
A validação é frequentemente tratada como uma cortesia de experiência do usuário: capture o e-mail vazio, mostre uma borda vermelha, siga em frente. Essa abordagem é o problema. Cada campo que sua API aceita é uma promessa que o chamador pode quebrar, e cada promessa quebrada é um caminho para sua lógica. Um parâmetro `limit` que você esperava ser um inteiro pequeno se torna `999999999`. Um `filename` que você esperava ser uma única palavra se torna `../../etc/passwd`. Um objeto `config` que você esperava conter configurações se torna um conjunto de instruções.
O teste de segurança não é uma disciplina separada adicionada no final. É o mesmo teste negativo que você já conhece, focado nos campos com maior probabilidade de causar danos. Se você criar o hábito de perguntar “qual é a pior coisa que se encaixa neste campo”, estará na maior parte do caminho em direção às práticas do nosso guia de melhores práticas de segurança de API. O restante deste artigo transforma essa única pergunta em testes concretos que você pode executar.
Como “carregar estes dados” se tornou “executar este código”
O incidente do Hugging Face é um exemplo claro do porquê a entrada merece essa atenção. O Hugging Face disse que o vetor de entrada eram conjuntos de dados maliciosos: um conjunto de dados manipulado acionou um carregador de conjunto de dados com execução de código remoto, e uma injeção de template estava dentro de uma configuração de conjunto de dados. Você pode ler o próprio relato da empresa em seu relatório de incidente de segurança.
Pense na natureza dessa falha. Um endpoint aceitou algo descrito como dados. O carregamento desses dados executou um caminho de código que poderia executar instruções controladas pelo atacante. “Carregar estes dados” se tornou “executar este código”. A injeção de template é a mesma história em uma escala menor: um valor de configuração que deveria ser um texto inerte foi avaliado, então o texto se tornou execução.
A principal conclusão não é “o Hugging Face cometeu um erro raro”. É que qualquer endpoint que aceita um nome de carregador, um formato, um template, um objeto serializado ou um bloco de configuração está aceitando instruções, quer você tenha pretendido ou não. Se você nunca escreveu um teste que envia uma configuração hostil para aquele endpoint, você nunca realmente verificou a suposição de que ela permanece inerte. Essa suposição não testada é a vulnerabilidade completa.
Validação de esquema como controle de segurança
O controle mais barato que você pode adicionar é um esquema rigoroso na borda. Um esquema não é apenas documentação. Quando você rejeita qualquer coisa que não corresponde, o esquema se torna um filtro que é executado antes mesmo que sua lógica de negócio veja a requisição. O JSON Schema oferece os primitivos para tornar esse filtro rigoroso.
Aqui está um esquema para a configuração do conjunto de dados da história, escrito de forma que a maioria das entradas hostis nunca chegue ao código da aplicação:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["loader", "name"],
"properties": {
"loader": { "enum": ["csv", "json", "parquet"] },
"name": { "type": "string", "maxLength": 128, "pattern": "^[\\w .-]+$" },
"rows": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
Leia-o como quatro defesas separadas. `additionalProperties: false` rejeita um campo `template` contrabandeado de forma direta, então um atacante não pode adicionar um. O enum `loader` significa que `pickle://` ou qualquer carregador de código remoto simplesmente não é um valor válido. `maxLength` elimina a string de vários megabytes destinada a esgotar a memória. O `pattern` em `name` recusa os caracteres `{{` e `'; DROP TABLE` antes que eles avancem. Nenhuma dessas linhas sabe sobre atacantes. Elas apenas aceitam o conjunto restrito de entradas que você realmente suporta, e essa restrição é a propriedade de segurança.
A validação de contrato como essa não captura todas as explorações, e nenhum esquema o fará. O que ela fecha é uma categoria específica e comum: o bug “nunca verificamos o que este endpoint aceita”. Essa categoria é onde um número surpreendente de violações começa.
Teste negativo: prove que o endpoint diz não
Testes de caminho feliz afirmam que uma entrada boa produz uma saída boa. Testes negativos afirmam que uma entrada ruim produz uma recusa controlada. A distinção importa porque uma recusa é um recurso: um 400 com um erro claro é sua API defendendo seu limite. Um 500 é sua API perdendo o controle dele.
Construa casos negativos da mesma forma todas as vezes. Para cada campo, escreva o que ele deve rejeitar: tipo incorreto, ausente quando obrigatório, presente quando proibido, muito longo, fora do intervalo e as strings de injeção que se encaixam em seu formato. Em seguida, afirme duas coisas na resposta. Primeiro, o status é um 4xx, geralmente 400 ou 422. Segundo, o status nunca é um 5xx. Um 500 significa que sua entrada hostil alcançou um código que não estava pronto para ela, o que é exatamente a acessibilidade que um atacante deseja. Nossa lista de verificação de testes de segurança de API tem uma lista inicial campo a campo que você pode adaptar.
Uma regra mantém isso honesto: afirme o comportamento, não o texto do erro. Se você afirmar que a mensagem diz “carregador inválido”, uma refatoração inofensiva quebra seu teste e ensina a equipe a afrouxá-lo. Afirme o código de status e, onde puder, afirme que nenhum efeito colateral aconteceu.
As classes de injeção que merecem um teste dedicado
Algumas famílias de injeção aparecem com frequência suficiente para que cada uma mereça casos de teste fixos, não uma verificação manual única. Você não precisa ser exaustivo aqui. Você precisa de um caso de sondagem por classe para que uma regressão falhe ruidosamente. Ferramentas que executam detecção automatizada de vulnerabilidades de API podem ampliar a cobertura mais tarde, mas um punhado de casos escritos manualmente encontra os furos óbvios primeiro.
- Injeção SQL: envie `1); DROP TABLE datasets;--` para qualquer campo que chegue a uma consulta. O endpoint deve tratá-lo como um valor literal e responder 400, ou retornar um resultado vazio, e nunca expor um erro de banco de dados.
- Injeção de template: envie `{{ 7*7 }}` e `{{ config.__class__ }}` para campos de nome e rótulo. Se a resposta contiver `49`, um motor de template avaliou sua entrada, e isso é código remoto prestes a acontecer.
- Desserialização insegura e carregadores de código remoto: envie um `loader` de `pickle://` ou um objeto serializado onde um valor simples deveria estar. Este é exatamente o padrão do Hugging Face. O endpoint deve rejeitar carregadores desconhecidos por meio de uma lista de permissões (allowlist), não tentar ser prestativo.
- Injeção de comando: envie `; id` e `$(id)` para qualquer campo que possa se tornar um argumento de shell, como um nome de arquivo ou uma opção de conversão. Um 200 que vaza um ID de usuário é uma descoberta crítica, não uma curiosidade.
Superdimensionado, malformado e confusão de tipo de conteúdo
Nem toda entrada hostil é uma string inteligente. Algumas são simplesmente muito grandes ou com formato incorreto, e estas frequentemente quebram os parsers antes mesmo que sua lógica de validação seja executada.
Envie um payload superdimensionado: um único campo contendo cinco megabytes de um caractere, ou um array JSON com um milhão de elementos. Uma API saudável impõe um limite de tamanho de corpo e retorna 413 em vez de alocar memória até travar. Envie corpos malformados também: JSON truncado, uma vírgula final ou JSON aninhado a mil níveis de profundidade para sondar por esgotamento de pilha. A resposta correta é um 400 rápido, não um worker travado.
A confusão de tipo de conteúdo é a discreta. Declare `Content-Type: application/json` mas envie XML, ou declare `application/xml` e envie um payload com uma entidade externa para sondar por XXE. Inverta a situação e envie JSON como `text/plain` para ver se um parser flexível o aceita de qualquer forma. Cada inconsistência testa se o seu servidor confia no cabeçalho, confia no corpo ou verifica se os dois concordam. Ele deve exigir concordância antes de analisar qualquer coisa.
Por que os agentes de IA aumentam as apostas
Tudo o que foi dito acima era verdade antes da existência dos agentes. Os agentes mudam o volume e a velocidade. Um atacante humano digita uma requisição hostil por vez. Um agente de IA gera e encaminha payloads na velocidade da máquina, e ele construirá alegremente entradas que uma pessoa nunca se daria ao trabalho de tentar.
Três propriedades pioram isso. Agentes sintetizam entrada, então produzem valores de campo que nenhum humano escreveu e nenhum teste antecipou. Agentes tentam novamente e encadeiam chamadas, então um único documento upstream envenenado pode se transformar em milhares de requisições hostis contra seu endpoint em segundos. E agentes encaminham dados nos quais foram instruídos a confiar, que é como um payload oculto em um conjunto de dados ou um webhook se torna uma requisição real para sua API. O padrão do Hugging Face, onde “carregar estes dados” se transforma em “executar este código”, é precisamente o tipo de instrução que um agente levará através de um limite de confiança sem perceber. Nossa nota sobre injeção de prompt para equipes de API se aprofunda nessa transferência. A defesa não muda; ela apenas precisa ser automática, porque você não pode revisar o tráfego de agentes manualmente.
Construa a suite negativa e execute-a no CI a cada mudança
Transforme os casos acima em uma suite que roda a cada pull request. Aqui está uma versão parametrizada compacta em pytest que atinge um endpoint de staging e afirma uma recusa controlada:
import httpx
import pytest
BASE = "https://staging.internal/v1"
HOSTILE_CONFIGS = [
{"loader": "pickle://s3/models/payload.pkl", "format": "auto"}, # remote-code loader
{"loader": "csv", "name": "{{ 7*7 }}"}, # template injection
{"loader": "csv", "name": "{{ config.__class__ }}"}, # object traversal
{"loader": "csv", "filter": "1); DROP TABLE datasets;--"}, # SQL injection
{"loader": "csv", "name": "A" * 5_000_000}, # oversized field
]
@pytest.mark.parametrize("config", HOSTILE_CONFIGS)
def test_dataset_config_is_refused(config):
r = httpx.post(f"{BASE}/datasets", json={"config": config}, timeout=10)
assert r.status_code in (400, 413, 422), r.text # a boundary that says no
assert r.status_code < 500, "5xx means the payload reached logic it should not"
assert "49" not in r.text, "template rendered: server-side template injection"
Conecte-o ao CI para que ele controle os merges. Um trabalho mínimo do GitHub Actions cumpre a função:
name: api-abuse-tests
on: [push, pull_request]
jobs:
negative-input:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pytest tests/negative_input.py -q
É aqui que uma ferramenta "schema-first" ganha seu lugar. No Apidog, você projeta o endpoint com base em um contrato OpenAPI, então cada requisição e resposta é verificada contra esse contrato enquanto você testa. Você pode salvar cenários negativos ao lado dos de caminho feliz: campos superdimensionados, tipos incorretos e as strings de injeção acima, cada um com uma afirmação de que o status é um 4xx. Em seguida, você executa os mesmos cenários no CI através da CLI do Apidog, para que uma mudança que silenciosamente afrouxa a validação falhe a build em vez de ser implantada. Se quiser experimentar, Baixe o Apidog e adicione um cenário negativo a um endpoint que você já possui.
Seja claro sobre o limite. O Apidog é uma ferramenta de design, teste, mock e documentação. Ele não executa um firewall de aplicativo web, filtra tráfego em tempo real ou substitui um SIEM, e a validação de contrato durante o teste não capturará todas as explorações. O que ele faz bem é tornar o contrato explícito e mantê-lo honesto sobre o que um endpoint aceita, para que a categoria “nunca verificamos” deixe de ser o que o surpreende em produção.
Perguntas frequentes
Qual a diferença entre teste negativo e fuzzing? O teste negativo envia um conjunto selecionado de entradas ruins que você escolheu propositalmente, uma para cada falha que lhe interessa. O fuzzing envia grandes volumes de entradas aleatórias ou mutadas para encontrar casos nos quais você não pensou. Comece com testes negativos porque eles são rápidos, determinísticos e fáceis de executar no CI. Adicione fuzzing quando quiser amplitude além da sua própria imaginação.
Esses testes devem ser executados em produção? Não. Execute-os em um ambiente de staging ou isolado. Alguns casos, como o payload superdimensionado ou uma sonda de injeção de comando, são projetados para estressar o sistema, e alguns podem alterar dados se um bug existir. Um ambiente de teste dedicado permite que os testes sejam agressivos sem nenhum risco para usuários reais.
Um firewall ou WAF não pegaria isso de qualquer forma? Um WAF é uma defesa em profundidade útil, mas não é um substituto para o aplicativo recusar entradas ruins. As regras podem ser contornadas, e um WAF não pode conhecer sua lógica de negócio. O objetivo desses testes é provar que o próprio endpoint diz não, para que você não dependa de um filtro que não controla totalmente.
Quantos casos negativos são suficientes por endpoint? Busque um caso por campo por classe de falha que ele pode sofrer: tipo incorreto, fora do intervalo, muito longo, campo proibido e qualquer string de injeção que se encaixe em seu formato. Isso geralmente é um punhado de casos por endpoint, não centenas. A cobertura das classes importa mais do que a contagem bruta.
A validação de esquema impede a injeção completamente? Não, e não deve ser sua única camada. Um esquema rigoroso remove uma grande parte de entradas malformadas e superdimensionadas e bloqueia campos inesperados, mas um valor pode ser válido para o esquema e ainda ser uma injeção SQL ou de template. Mantenha consultas parametrizadas, desserialização segura e codificação de saída em vigor, e use o esquema para reduzir a superfície que essas camadas precisam defender.
