Este guia detalha o método que realmente funciona: restrição estrutural para SELECT, lista de permissões fechada de funções, limite de linha obrigatório e bloqueio do esquema consultado. Cada etapa depende do validador realmente implementado no mecanismo NL2SQL do Aurabase, um recurso de sua IA nativa incorporada ao back-end, e não um serviço de terceiros criado após o fato. Se o assunto for novo para você, nossa visão geral do NL2SQL estabelece as bases, e o tutorial passo a passo mostra como construir o endpoint completo.
O essencial
- A engenharia de prompt (“gera apenas SELECT”) não é uma verificação de segurança: um modelo pode ter alucinações, ser guiado por uma pergunta ambígua ou simplesmente ignorar as instruções.
- A validação válida é estrutural: um analisador constrói a árvore sintática (AST) da solicitação e rejeita por padrão qualquer coisa que não seja explicitamente autorizada.
- Quatro camadas concretas limitam o risco: SELECT estrito (nem subconsulta, nem CTE, nem UNION), lista de permissões fechada de dez funções,
LIMITobrigatório e limitado, acesso bloqueado ao catálogo do sistema e esquemas não locatários. - O esquema consultado deve vir do servidor , nunca de um campo na solicitação do cliente: caso contrário, nada impede que um chamador forneça seu próprio esquema para ignorar a validação.
- Na Aurabase, este validador (Rust crate
sqlparser) é testado com casos adversários documentados no código: funções proibidas escondidas emFILTER, em um intra-agregadoORDER BY, ou emOFFSET.
Por que uma instrução no prompt do sistema não bloqueia nada?
Um prompt do sistema que diz "gera apenas consultas SELECT" é uma preferência, não uma barreira. O modelo o respeita na maioria das vezes porque foi treinado para seguir as instruções, não porque uma restrição técnica o impeça fisicamente de escrever qualquer outra coisa. Duas classes de fracasso tornam esta confiança insuficiente na produção.
A primeira vem da própria pergunta. Um usuário, mal intencionado ou simplesmente criativo em sua formulação, pode direcionar a questão de forma a empurrar o modelo para SQL que ele não deveria ter escrito: uma junção a uma tabela sensível, um filtro que contorna a lógica esperada, uma chamada de função do sistema. O modelo não distingue entre uma questão legítima e outra destinada a manipulá-la.
A segunda não requer malícia. Um modelo pode alucinar um nome de tabela, esquecer o LIMIT que o prompt solicitou ou gerar um SELECT * sem quaisquer restrições em uma tabela grande. O resultado é o mesmo em ambos os casos: SQL potencialmente caro ou intrusivo, que passou no filtro de prompt e está prestes a ser executado em um banco de dados real.
O prompt do sistema continua útil, pois direciona o modelo para o resultado correto na maioria das vezes. Mas uma placa de “acesso proibido” não impede ninguém que não saiba ler ou que decida ignorá-la. Você precisa de uma porta fechada atrás, não apenas de um painel na frente.
Analise o SQL gerado em uma árvore de sintaxe, nunca em uma string bruta
A primeira linha de defesa é analisar o SQL produzido pelo modelo com um analisador real para o dialeto alvo e, em seguida, validar a estrutura resultante, não o texto bruto. Uma busca por palavras proibidas na cadeia de caracteres (“DROP”, “DELETE”, “;”) é trivialmente contornada: maiúsculas e minúsculas, comentário inserido no meio de uma palavra-chave, aspas digitadas. Uma árvore de sintaxe descreve inequivocamente o que a consulta realmente faz.
Aurabase implementa esta etapa com a caixa Rust sqlparser e seu dialeto PostgreSqlDialect. Mesmo antes da análise, um primeiro filtro lexical rejeita duas construções que são difíceis de raciocinar corretamente uma vez na árvore: aspas de dólar ($$...$$), que podem ocultar conteúdo arbitrário em uma string, e comentários de várias linhas (/* */), que podem ocultar o final real de uma instrução.
Essa rejeição apenas de múltiplas instruções bloqueia a forma mais conhecida de injeção de SQL por empilhamento: SELECT * FROM users; DROP TABLE users;--. O analisador retorna apenas uma instrução acionável, a segunda simplesmente nunca é alcançada, não importa como esteja redigida na pergunta original.
Restringir estruturalmente a um simples SELECT
Uma vez obtida a árvore, a validação mais ampla é aceitar apenas um tipo de nó raiz, uma consulta (Statement::Query), e rejeitar todo o resto: INSERT, UPDATE, DELETE, DROP, CREATE, ALTER. Não é mais uma instrução imediata, é uma condição sobre o tipo do objeto analisado, que nenhuma formulação hábil da questão pode contornar.
Mesmo dentro de um SELECT, diversas construções permanecem perigosas e merecem sua própria rejeição explícita:
| Construção rejeitada | Por que é perigoso? |
|---|---|
| CTE / COM | Pode encadear lógica não intencional adicional antes do SELECT final. |
| Subconsultas, UNION / INTERSECT / EXCEPT | Expande a área de superfície do que uma única pergunta pode fazer em uma única consulta. |
| SELECIONE...EM | Cria uma tabela: escrita disfarçada de leitura. |
| PARA ATUALIZAR / PARA COMPARTILHAR | Instalação de fechaduras, risco de contenção com tráfego de produção. |
| Funções de tabela (generate_series, pg_read_file...) | Acesso ao sistema ou negação de serviço através de linhas geradas sob demanda. |
Um caso de teste retirado do repositório ilustra concretamente o último ponto: SELECT * INTO backup FROM users é rejeitado, mesmo que não contenha palavra-chave de escrita visível nem função suspeita. A forma do pedido é suficiente para desqualificá-lo.
Lista de permissões de funções, não lista negra
Uma lista negra de funções proibidas (pg_sleep, pg_read_file, dblink...) requer a antecipação de cada função perigosa, uma por uma, enquanto o Postgres expõe várias centenas delas. Uma lista de permissões inverte o ônus da prova: apenas dez funções são autorizadas, count, sum, avg, min, max, lower, upper, coalesce, date_trunc, now. Todo o resto não é permitido por padrão, incluindo um recurso legítimo que ninguém pensou em adicionar ainda.
Uma única passagem de validação nem sempre é suficiente. Uma travessia estrutural da árvore lista seus pontos de entrada um por um (projeção, WHERE, JOIN, GROUP BY...), e é fácil esquecer um: uma função proibida pode estar oculta em uma cláusula FILTER (WHERE pg_sleep(10) IS NOT NULL), em um ORDER BY intra-agregado (sum(id ORDER BY pg_sleep(10))), em WITHIN GROUP, DISTINCT ONou OFFSET.
O validador Aurabase adiciona portanto uma segunda passagem exaustiva, que percorre todas as expressões da árvore onde quer que estejam, independentemente do caminho estrutural. É uma defesa assumida em profundidade: se o primeiro passe erra um caso, o segundo o alcança.
Limite as linhas retornadas: LIMIT obrigatório e limitado
SELECT * permanece autorizado, é útil para mineração de dados. O risco não é a estrela, é a ausência de teto em uma consulta escrita por um modelo: uma pergunta mal formulada pode trazer de volta uma tabela inteira, com o custo de memória e o tempo de resposta que isso implica.
Aurabase aplica uma regra simples e transparente. Caso o SQL gerado não possua um LIMIT, o servidor adiciona um (100 linhas por padrão, valor anunciado ao modelo no prompt do sistema). Se o SQL solicitar um LIMIT além do limite máximo (1.000 linhas por padrão), a consulta será recusada explicitamente em vez de reduzida silenciosamente. Ambos os valores são configuráveis no lado do servidor (AI_NL2SQL_DEFAULT_LIMIT, AI_NL2SQL_MAX_LIMIT), e o servidor até se recusa a iniciar se a falha ultrapassar o teto.
Recusar, em vez de reduzir silenciosamente, tem um interesse directo: um limite máximo aplicado sem o declarar daria ao interlocutor a ilusão de que o seu pedido foi atendido, enquanto o resultado teria sido truncado sem que ele soubesse. limit_injected sempre diz se o valor vem do modelo ou do servidor.
Bloquear o acesso ao esquema: catálogo do sistema e esquema cruzado
Dois vazamentos distintos ameaçam um mecanismo NL2SQL conectado a um banco de dados real: acesso ao catálogo do sistema Postgres e acesso a um esquema que não pertence ao chamador. Ambos são bloqueados na validação, independentemente de qualquer política de RLS colocada no downstream.
pg_catalog sempre faz parte de search_path, o que significa que um nome não qualificado como pg_authid ou pg_stat_activity o acessa diretamente, sem prefixo. O validador Aurabase bloqueia qualquer nome que comece com pg_, bem como information_schema e o esquema interno aura_console, qualificado ou não.
Em um nome de dois componentes (schema.table), apenas o esquema do projeto chamador é permitido, qualquer outro valor é rejeitado. Um nome com três ou mais componentes é automaticamente recusado. Esse limite no nível da consulta gerada é um acréscimo ao isolamento no nível do banco de dados detalhado em nosso artigo sobreisolamento multilocatário: um impede que o SQL gerado tenha como alvo outro esquema, o outro evita que a própria conexão alcance outro banco de dados. Nenhum substitui o outro.
Nunca deixe o cliente redefinir o esquema consultado
Uma armadilha discreta aguarda qualquer API NL2SQL que aceite um parâmetro que descreve o esquema ou tabelas permitidas na consulta do cliente. Se esse mesmo parâmetro for usado para construir o prompt e validar o SQL de saída, um chamador poderá mentir sobre o que é permitido, e a validação será validada contra essa mentira, e não contra a realidade do banco de dados.
Aurabase examina o esquema base real do projeto em cada chamada, com um cache curto de trinta segundos para desempenho, e rejeita explicitamente (erro 400) qualquer campo schema, allowed_schemaou schema_context enviado no corpo da solicitação, em vez de aceitá-lo e substituí-lo silenciosamente. A diferença importa: um campo aceite e depois ignorado dá a ilusão de um controlo que não existe; um campo recusado diz isso imediatamente.
Audite seu próprio pipeline NL2SQL antes da produção
Quer você use o Aurabase ou construa seu próprio pipeline com base em um LLM genérico, os pontos a seguir cobrem o que é esquecido com mais frequência.
Se você mesmo escrever o validador
- Analise SQL com um analisador real para o seu dialeto exato, nunca com correspondência de padrões em uma string.
- Adote uma rejeição padrão: qualquer tipo de nó, qualquer função não autorizada explicitamente deve ser recusada, não apenas os casos perigosos já identificados.
- Aceite apenas uma instrução por consulta; esta é a rejeição mais simples contra o empilhamento de consultas.
- Teste o validador com casos reais de adversário (função proibida em FILTER, em ORDER BY intra-agregado, em OFFSET), não apenas com casos óbvios.
- Apesar de tudo, execute o SQL validado com uma função Postgres com privilégios reduzidos no esquema esperado: o validador limita a forma da consulta, a função limita o que pode alcançar fisicamente se um caso lhe tiver escapado.
Se você estiver avaliando uma estrutura NL2SQL de terceiros
- Pergunte explicitamente se a validação é estrutural (AST) ou apenas uma instrução imediata: a resposta muda tudo.
- Verifique se um limite de linha é aplicado por padrão, e não apenas documentado como uma prática recomendada às suas custas.
- Verifique se o esquema utilizado para validação pode ser fornecido pelo cliente API, o que reabriria exatamente a falha descrita acima.
- Compare várias ferramentas neste critério específico antes de escolher: nossa comparação de ferramentas NL2SQL detalha o que distingue as abordagens disponíveis em 2026.
O validador reduz o risco, não substitui o RLS
Um validador AST sólido reduz o risco na origem: o SQL que chega ao seu banco de dados já possui um formato conhecido e limitado. No entanto, ela não substitui as políticas RLS nas suas tabelas confidenciais, que decidem quais linhas um determinado usuário tem o direito de ver. As duas camadas respondem a questões diferentes: o validador limita a forma da consulta gerada, o RLS limita os dados que pode retornar para um usuário específico. Mantenha ambos ativos, mesmo que um pareça redundante com o outro.
NL2SQL cobre questões estruturadas sobre suas tabelas. Para perguntas sobre conteúdo não estruturado, documentos, notas, tickets, o RAG nativo do Aurabase segue uma lógica de segurança comparável, detalhada em nosso tutorial de pipeline RAG em pgvector.