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.

Objeto escuro em bandeja vermelha numa antecâmara entre portões desencontrados, separado de um compartimento interno.
Ilustração conceitual. Conteúdo externo e execução precisam atravessar fronteiras independentes de validação e isolamento antes de alcançar destinos.

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

Captura real da página local que exibe a saída do laboratório Python com SQLite e dados sintéticos.
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.
Captura real do visualizador local de saída, não da interface do ChatGPT. Código publicado reexecutado no QA desta revisão, com dados sintéticos; não é homologação de produção.

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

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.