Um nome de cliente pode alterar uma consulta mal construída
O agente recebe a tarefa de consultar pedidos de um cliente. A ferramenta junta o código recebido a uma string SQL e executa a consulta. Se aquele valor contiver sintaxe SQL, ele pode mudar o filtro e devolver registros que a operação não deveria alcançar.

Esse defeito pode existir mesmo quando o modelo segue as instruções. Um valor copiado de e-mail, documento ou cadastro atravessa a ferramenta e passa a ser interpretado pelo banco como parte do comando.
SQL parametrizado mantém os valores separados da estrutura da consulta. Para uma ferramenta recorrente, a aplicação define o SQL e oferece ao agente um conjunto pequeno de argumentos. O contrato também precisa controlar identidade, escopo, ordenação e volume.
Este artigo entrega um laboratório local dessa fronteira. A decisão anterior, sobre acesso direto ao banco, API ou camada semântica, continua dependendo do processo. Aqui, o objetivo é verificar como uma consulta conhecida recebe argumentos sem permitir que eles reescrevam o comando.
O que a parametrização protege
A OWASP recomenda prepared statements e consultas parametrizadas para separar código e dados. A documentação do módulo sqlite3 do Python orienta usar placeholders, em vez de formatação de strings, ao vincular valores.
Considere este desenho, inadequado para dados não confiáveis:
sql = f"SELECT id FROM pedidos WHERE cliente = '{cliente}'"
Na versão parametrizada com sqlite3, a estrutura fica fixa:
sql = "SELECT id FROM pedidos WHERE cliente = ?"
linhas = conexao.execute(sql, (cliente,)).fetchall()
O driver recebe a consulta e os valores separadamente. Um apóstrofo no nome deixa de ser uma oportunidade para fechar a string SQL. O marcador varia conforme o driver; não copie ? para uma biblioteca que exige outra convenção.
A proteção vale para os valores vinculados. Ela não confirma que o solicitante pode consultar determinado cliente, que a métrica está correta ou que a consulta cabe no orçamento do banco.
Identificadores precisam de outro tratamento
Tabela, coluna e direção de ordenação normalmente não podem ser substituídas por parâmetros de valor. ORDER BY ? não deve ser usado como tentativa de selecionar dinamicamente uma coluna.
Quando houver escolha legítima, mapeie um rótulo externo para uma expressão mantida no código:
ORDENS = {
"id_crescente": "id ASC",
"id_decrescente": "id DESC",
}
A ferramenta aceita um dos rótulos e rejeita os demais. Somente a expressão constante do mapa entra no SQL. Esse é um uso delimitado de lista de permissão, coerente com a orientação da OWASP para trechos que não admitem bind variables.
Evite receber tabela, where, join ou sql_completo como argumentos de uma ferramenta cujo trabalho é apenas listar pedidos. O catálogo de ferramentas deve registrar a operação de negócio e seus efeitos, incluindo os caminhos que ela não oferece.
A identidade do cliente não deve vir do modelo
Parametrizar tenant_id protege sua sintaxe. Se o agente puder escolher livremente esse valor, ainda poderá solicitar outro cliente usando um identificador perfeitamente válido.
O serviço precisa obter o escopo de uma identidade autenticada e autorizada. Em seguida, associa a chamada à unidade permitida. Dados extraídos de texto podem fornecer um filtro dentro desse escopo; não devem definir a própria fronteira.
Para ambientes multicliente, mantenha controles adicionais no banco ou na arquitetura. A validação de RLS no PostgreSQL cobre políticas por papel e comando. O laboratório abaixo usa SQLite e não demonstra RLS, autenticação real ou isolamento de um pool de produção.
Laboratório reproduzível com dados sintéticos
Requisitos: Python 3 com o módulo padrão sqlite3, terminal e uma máquina de desenvolvimento. O programa usa um banco em memória, sem rede, credenciais empresariais ou instalação de pacotes.
Copie o bloco para laboratorio_sql.py e execute python3 laboratorio_sql.py. O trecho vulnerável existe apenas para comparação nessa base descartável. Nunca o conecte a dados reais.
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE pedidos (id TEXT, tenant TEXT, cliente TEXT)")
con.executemany(
"INSERT INTO pedidos VALUES (?, ?, ?)",
[("0001", "A", "C01"), ("0002", "A", "C02"),
("0003", "B", "C01")],
)
con.commit()
con.execute("PRAGMA query_only = ON")
ORDENS = {"id_crescente": "id ASC", "id_decrescente": "id DESC"}
ESCOPOS = {"principal_demo": "A"} # Contexto fictício do servidor, sem autenticação real.
def listar(principal, cliente, ordem="id_crescente", limite=20):
tenant = ESCOPOS.get(principal)
if tenant is None:
raise PermissionError("identidade sem escopo")
if not isinstance(cliente, str) or len(cliente) > 80:
raise ValueError("cliente invalido")
if ordem not in ORDENS:
raise ValueError("ordem nao permitida")
if type(limite) is not int or not 1 <= limite <= 50:
raise ValueError("limite invalido")
sql = (
"SELECT id FROM pedidos WHERE tenant = ? AND cliente = ? "
f"ORDER BY {ORDENS[ordem]} LIMIT ?"
)
return con.execute(sql, (tenant, cliente, limite)).fetchall()
def rejeita(erro, chamada):
try:
chamada()
except erro:
return
raise AssertionError("a chamada deveria ter sido rejeitada")
hostil = "C01' OR 1=1 --" # Comparação vulnerável apenas na base fictícia em memória.
inseguro = con.execute(
f"SELECT id FROM pedidos WHERE tenant = 'A' AND cliente = '{hostil}'"
).fetchall()
assert len(inseguro) == 3
assert listar("principal_demo", hostil) == []
assert listar("principal_demo", "C01") == [("0001",)]
assert listar("principal_demo", "C02", "id_decrescente", 1) == [("0002",)]
rejeita(ValueError, lambda: listar("principal_demo", "C01", "id; DROP TABLE pedidos"))
rejeita(ValueError, lambda: listar("principal_demo", "C01", limite=51))
rejeita(ValueError, lambda: listar("principal_demo", "C01", limite=True))
rejeita(PermissionError, lambda: listar("desconhecido", "C01"))
rejeita(sqlite3.OperationalError, lambda: con.execute("DELETE FROM pedidos"))
assert con.execute("SELECT count(*) FROM pedidos").fetchone()[0] == 3
print("Comparacao insegura:", len(inseguro))
print("Valor hostil parametrizado:", len(listar("principal_demo", hostil)))
print("Cliente C01 no escopo A:", listar("principal_demo", "C01"))
print("Testes locais concluidos; base sintetica preservada.")
con.close()
O programa inclui uma testemunha positiva: consultar C01 devolve 0001, sem devolver 0003, que pertence a outro escopo. Um teste que bloqueia todas as consultas pode parecer seguro e continuar inutilizável.
O limite de 50 registros é uma escolha didática. O valor adequado depende do processo. O uso de type(limite) is int também impede que True seja aceito como inteiro nesse contrato.
Execução local do laboratório publicado

Ler a saída em texto
Comparacao insegura: 3
Valor hostil parametrizado: 0
Cliente C01 no escopo A: [('0001',)]
Testes locais concluidos; base sintetica preservada.
Resultado observado e alcance da prova
Em 23 de outubro de 2026, na preparação editorial original, foi executado esse bloco localmente. A consulta concatenada devolveu os três pedidos da base fictícia. A chamada parametrizada com o mesmo valor hostil devolveu zero linhas. A consulta legítima retornou apenas 0001 no escopo A. As rejeições e a preservação dos três registros passaram pelas asserções.
Esse resultado demonstra o comportamento do código publicado com Python 3.14.7 e SQLite 3.53.1. Não houve chamada a um modelo, PostgreSQL, CRM ou banco empresarial.
PRAGMA query_only restringe alterações naquela conexão do exercício. Em produção, use permissões reais e uma credencial sem autoridade de escrita. Se o mesmo processo puder abrir conexões amplas ou reconfigurar a proteção, a fronteira continua insuficiente.
Controles que precisam acompanhar a consulta
A homologação deve verificar também:
- autorização do usuário para a operação e para cada objeto consultado;
- colunas devolvidas, com exclusão de informação desnecessária;
- tempo de execução, concorrência e custo do plano;
- tamanho do resultado em bytes, além da quantidade de linhas;
- tratamento de erros sem divulgar schema ou credenciais;
- retenção de parâmetros e resultados nos logs;
- atualização da fonte e significado dos filtros.
LIMIT controla linhas devolvidas, mas não garante uma varredura barata. Uma consulta pode examinar uma tabela grande antes de produzir poucas linhas. Meça o plano e configure timeouts no ambiente real.
Na saída, preserve identidade da consulta aprovada, versão, horário, escopo e quantidade de registros. Parâmetros sensíveis devem receber proteção específica, sem serem copiados indiscriminadamente para logs ou contexto. O log de auditoria de agentes ajuda a definir essa trilha.
Como transformar o laboratório em um gate de implantação
Para cada ferramenta SQL, registre o template autorizado, os argumentos, a origem da identidade, a lista de ordenações, o teto de resultado e o papel usado no banco. Faça os testes no driver e na configuração que chegarão à produção.
Inclua valores com apóstrofo, campo vazio, código inexistente, limite fora da faixa, coluna não permitida e identidade sem acesso. Confira tanto os casos recusados quanto o acesso legítimo. Se existir SQL livre em outra ferramenta, ele precisa de avaliação própria; proteger uma função não protege todas as rotas do agente.
Uma revisão humana de SQL gerado pode ajudar na investigação, mas a ferramenta recorrente deve aplicar a barreira independentemente dessa revisão. O ambiente de teste para agentes permite exercitar essas condições sem expor produção.
A liberação exige que a equipe consiga apontar onde cada valor entra, quem define o escopo e qual controle impede a execução fora do contrato. Se uma dessas respostas depender apenas do prompt, mantenha a ferramenta fora da operação real.
Fontes consultadas em 23 de outubro de 2026
- OWASP: SQL Injection Prevention Cheat Sheet, para parametrização, identificadores permitidos e menor privilégio.
- Python: sqlite3 e vinculação de valores com placeholders, para o mecanismo usado no laboratório.
O procedimento de implantação é uma proposta operacional da publicação original. A demonstração local cobre a montagem e os testes publicados, sem substituir auditoria da aplicação, das credenciais e do banco de destino.