29 de dez. de 2025·4 min de leitura

Injeção de SQL em apps CRUD gerados por IA: padrões e correções

Saiba identificar injeção de SQL em apps CRUD gerados por IA, com exemplos concretos de consultas vulneráveis e substituições mais seguras usando parâmetros e recursos de ORM.

Injeção de SQL em apps CRUD gerados por IA: padrões e correções

O que a injeção de SQL parece em apps CRUD

A injeção de SQL acontece quando um app permite que a entrada do usuário mude o significado de uma consulta ao banco de dados. Em vez de ser tratada como dado simples, a entrada passa a fazer parte do SQL. Isso pode expor dados privados, alterar registros ou apagar tabelas.

Endpoints CRUD são alvos comuns porque aceitam entrada constantemente: caixas de busca, filtros, formulários de edição, IDs em URLs e telas administrativas. Atacantes não precisam de nada sofisticado — basta um lugar em que a entrada seja costurada numa consulta.

Em muitos protótipos gerados por IA, os pontos arriscados parecem “normais” à primeira vista. Geradores de código frequentemente recorrem à concatenação rápida de strings, especialmente em busca, filtros e ordenação. Eles também tendem a concentrar parsing de requisição, construção de consulta e formatação de resposta numa única função longa, o que torna fácil perder uma linha insegura.

Sinais de que uma rota CRUD pode ser injetável:

  • Strings SQL construídas com +, template strings, ou replace usando valores da requisição
  • Texto de busca inserido diretamente em LIKE '%...%'
  • ORDER BY dinâmico ou nomes de colunas vindos de query params
  • Endpoints administrativos que confiam nas entradas por serem “internos”
  • Erros que exibem SQL cru ou detalhes do banco de dados

Uma regra simples funciona bem: se o banco pode interpretar a entrada do usuário como palavras-chave SQL (como OR, UNION, DROP), você não está realmente protegido. Se o banco só vê a entrada como valores ligados a parâmetros, você está no caminho seguro.

Padrões vulneráveis comuns que ferramentas de IA produzem

A injeção tende a aparecer nos mesmos lugares: busca, filtragem, ordenação e paginação. Essas áreas parecem manipulação inofensiva de strings, mas tocam o banco em quase toda requisição.

Um padrão clássico é concatenar a entrada do usuário numa cláusula WHERE:

// Vulnerable
const q = req.query.q;
const sql = "SELECT * FROM users WHERE email = '" + q + "'";
await db.query(sql);

Outro padrão comum é construir “filtros opcionais” adicionando fragmentos brutos num loop, especialmente LIKE '%${q}%' e status = '${status}'. O risco cresce rápido assim que um campo é esquecido ou “sanitizado” com um replace fraco.

Ordenação e paginação são sobras frequentes. As pessoas procuram injeção no WHERE, depois esquecem que ORDER BY e LIMIT frequentemente vêm direto dos query params:

// Vulnerable
const sort = req.query.sort;     // e.g. "created_at DESC"
const limit = req.query.limit;   // e.g. "50"
const sql = `SELECT * FROM orders ORDER BY ${sort} LIMIT ${limit}`;
await db.query(sql);

Você também verá atalhos repetidos em protótipos:

  • Escapar em alguns lugares, mas esquecer em um endpoint
  • Permitir nomes de coluna arbitrários para ordenação “flexível”
  • Tratar IDs como seguros porque “são números”, e depois usá-los como strings
  • Logar SQL cru com entrada do usuário (o que pode vazar dados sensíveis)
  • Copiar e colar um padrão de consulta inseguro em várias rotas

Se você herdou um app gerado por IA de ferramentas como Bolt, v0, Cursor ou Replit, assuma que esses padrões existem até provar o contrário.

Exemplo concreto: corrigindo SQL cru com parâmetros

Interpolação de strings costuma parecer limpa, mas mistura código e entrada do usuário na mesma string.

Exemplo vulnerável:

// GET /users?email=...
const email = req.query.email;
const sql = `SELECT id, email FROM users WHERE email = '${email}'`;
const rows = await db.query(sql);

Se alguém passar x' OR '1'='1, a consulta pode retornar todos os usuários. A correção é manter o texto SQL estático e passar os valores separadamente.

Substituição segura: placeholders + valores separados

Placeholders do PostgreSQL:

const email = req.query.email;
const sql = "SELECT id, email FROM users WHERE email = $1";
const rows = await db.query(sql, [email]);

Placeholders do MySQL/SQLite:

const sql = "SELECT id, email FROM users WHERE email = ?";
const rows = await db.query(sql, [email]);

O ponto é simples: a entrada não é colada na string SQL. O driver envia como dado, não como código.

Casos extremos: listas IN (...) e valores vazios

Filtros como “status in [a, b, c]” frequentemente são corrigidos de forma incorreta.

Inseguro:

const statuses = req.query.statuses; // e.g. "active,paused"
const sql = `SELECT * FROM users WHERE status IN (${statuses})`;

Mais seguro: construa placeholders e ainda passe valores separadamente.

const statuses = (req.query.statuses || "")
  .split(",")
  .map(s => s.trim())
  .filter(Boolean);

if (statuses.length === 0) return res.json([]); // ou pule o filtro

const placeholders = statuses.map((_, i) => `$${i + 1}`).join(", ");
const sql = `SELECT * FROM users WHERE status IN (${placeholders})`;
const rows = await db.query(sql, statuses);

Regras práticas que evitam regressões bagunçadas:

  • Trate strings vazias como “sem filtro”, não como texto SQL
  • Valide tipos cedo (números devem ser números antes da consulta)
  • Nunca aceite fragmentos SQL brutos da requisição, mesmo os “inofensivos”
  • Mantenha a construção de consultas em um só lugar para que as correções se mantenham

Exemplo concreto: filtragem segura sem construir SQL com strings

Um padrão comum de injeção é transformar “filtros opcionais” numa string WHERE crescente.

Formato vulnerável:

// ❌ Vulnerable: string-built WHERE
let where = "WHERE 1=1";
if (q) where += ` AND name ILIKE '%${q}%'`;
if (minPrice) where += ` AND price >= ${minPrice}`;
if (startDate) where += ` AND created_at >= '${startDate}'`;

const sql = `SELECT * FROM products ${where} ORDER BY created_at DESC LIMIT ${limit}`;

Padrão mais seguro: construa condições separadamente, mantenha a entrada do usuário fora do texto SQL e passe valores como parâmetros.

// ✅ Safe: conditions + params
const conditions = [];
const params = [];

if (q) {
  params.push(`%${q}%`);
  conditions.push(`name ILIKE $${params.length}`);
}

if (minPrice) {
  params.push(Number(minPrice));
  conditions.push(`price >= $${params.length}`);
}

if (startDate) {
  params.push(new Date(startDate));
  conditions.push(`created_at >= $${params.length}`);
}

const whereSql = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
const sql = `SELECT * FROM products ${whereSql} ORDER BY created_at DESC LIMIT 50`;
const rows = await db.query(sql, params);

Alguns detalhes que evitam bugs sutis:

  • Coloque curingas % no parâmetro, não na string SQL
  • Parseie e valide datas, depois vincule-as como parâmetros
  • Converta faixas numéricas e rejeite NaN antes de vincular
  • Se um filtro é opcional, omita a condição completamente

Exemplo concreto: ordenação e paginação seguras

Feche brechas no painel administrativo
Corrigimos autenticações falhas e apertamos acessos para que ferramentas administrativas não virem ponto de entrada.

Ordenação é onde endpoints que já usam parâmetros costumam ficar arriscados. Valores como texto de busca podem ser parametrizados. Nomes de coluna e direção de ordenação não podem.

Padrão seguro: mapeie a entrada do usuário para uma pequena allowlist de colunas e direções conhecidas e rejeite o resto.

// Example: Node/Express with Postgres (pg)
const SORT_FIELDS = {
  createdAt: 'created_at',
  email: 'email',
  status: 'status'
};

function buildListUsersQuery({ sort = 'createdAt', dir = 'desc', page = 1, pageSize = 20 }) {
  const field = SORT_FIELDS[sort];
  if (!field) throw new Error('Invalid sort field');

  const direction = String(dir).toLowerCase() === 'asc' ? 'ASC' : 'DESC';

  const limit = Math.min(Math.max(parseInt(pageSize, 10) || 20, 1), 100);
  const offset = Math.max((parseInt(page, 10) || 1) - 1, 0) * limit;

  // Only the allowlisted identifier and direction are interpolated.
  // Pagination values stay parameterized.
  return {
    text: `SELECT id, email, status, created_at FROM users ORDER BY ${field} ${direction} LIMIT $1 OFFSET $2`,
    values: [limit, offset]
  };
}

Fluxo de correção passo a passo para um app CRUD existente

Quando um app CRUD “funciona na maior parte”, a injeção frequentemente se esconde nas bordas: busca, filtros, ordenação, painéis administrativos e ações em massa. Um fluxo de correção evita que você conserte um endpoint e deixe três abertos.

  1. Faça inventário de toda consulta e caminho de entrada. Liste endpoint, tipo de consulta (read/write), de onde vem a entrada e quais campos alcançam o banco. Inclua jobs em background e ferramentas administrativas.

  2. Substitua SQL construído por strings por parâmetros. Procure concatenação de consultas e troque por consultas parametrizadas ou um query builder. Faça isso mesmo para endpoints “internos”.

  3. Adicione allowlists para identificadores. Você não pode parametrizar com segurança nomes de colunas, tabelas ou direções de ordenação. Se a entrada do usuário controla ORDER BY, colunas selecionadas ou joins, faça um mapeamento para identificadores conhecidos e seguros.

  4. Adicione alguns testes focados. Envie payloads que tentem quebrar citações (uma aspa simples), truques booleanos comuns (OR 1=1) e tipos inesperados. Asserte comportamento seguro: sem linhas extras, sem vazamento de dados, sem exposição de erros SQL.

  5. Rever logs e tratamento de erros. Dispare erros de propósito e confirme que as respostas não incluem SQL cru, stack traces ou detalhes do driver. Mantenha erros detalhados nos logs do servidor e redija valores que possam conter dados sensíveis.

Uso mais seguro de ORMs (e armadilhas a evitar)

Um ORM pode bloquear injeção, mas só se você usar seus caminhos seguros. Isso geralmente significa deixar o ORM construir SQL enquanto você passa entradas do usuário como valores.

Padrões “seguros” parecem “filtrar por esses campos”, não “construir uma string SQL”.

// Example: safe parameter binding (generic)
const users = await db.user.findMany({
  where: {
    email: inputEmail,   // value is bound, not concatenated
    isActive: true
  },
  take: 25
});

// Example: query builder style with placeholders
const users2 = await knex('users')
  .where('email', '=', inputEmail)
  .andWhere('is_active', '=', true)
  .limit(25);

ORMS ainda têm saídas de emergência, e código gerado por IA costuma abusar delas. Tenha cuidado com:

  • Helpers de query raw com interpolação de string
  • Métodos explicitamente rotulados como “unsafe”
  • Passar entrada do usuário em identificadores (nomes de colunas, tabelas)
  • APIs que aceitam um fragmento SQL como string em vez de um valor

Se precisar usar SQL cru, use o recurso de parâmetros do ORM, não template strings. Para nomes de coluna dinâmicos, use uma whitelist.

Erros comuns ao corrigir injeção de SQL

Previna regressões de injeção
Pare de copiar padrões SQL inseguros definindo padrões mais seguros em todo o projeto.

Validação de entrada não é suficiente. Bloquear alguns caracteres ou aparar espaços pode reduzir ruído, mas não impede injeção se um valor ainda acabar dentro de uma string SQL.

Escapar manualmente é outra armadilha. Parece mais seguro substituir aspas, mas regras de escape variam entre bancos e casos limites são fáceis de perder. Consultas parametrizadas são mais seguras e fáceis de revisar.

Prepared statements também podem ser usados errado. Um erro frequente é parametrizar o valor de busca, mas continuar concatenando pedaços SQL como ORDER BY:

const sql = `SELECT * FROM users WHERE name ILIKE $1 ORDER BY ${sort} ${dir}`;
await db.query(sql, [`%${q}%`]);

Se sort ou dir vier da requisição, um atacante pode escapar. Corrija com uma allowlist estrita para identificadores.

Logs são um risco subestimado. Logar SQL completo mais entrada do usuário pode vazar emails, tokens ou outros segredos copiados numa busca. Mantenha logs em nível alto e mascarar campos sensíveis.

Exemplo realista: endpoint de busca administrativo vulnerável

Um fundador publica rápido um painel administrativo gerado por IA. Tem uma caixa de busca simples: “Search users by email or name.” O backend constrói uma string SQL com o que o admin digita e executa como está.

Parece inofensivo por ser “apenas admin”. Mas endpoints administrativos se expõem no mundo real: rota mal configurada, cookie vazado, senha fraca ou uma ferramenta interna acidentalmente deployada em URL pública.

A injeção acontece quando a entrada de busca é colocada dentro de uma consulta assim:

SELECT id, email, role FROM users WHERE email LIKE '%{q}%' OR name LIKE '%{q}%'

Um payload realista fecha a aspa e adiciona uma condição verdadeira, como %' OR 1=1 --. Agora a cláusula WHERE sempre casa, e a resposta pode despejar muito mais dados do que o pretendido.

Numa versão corrigida, a mesma busca usa parâmetros. Se alguém enviar %' OR 1=1 --, isso é tratado como texto simples e se comporta como uma busca normal (geralmente sem resultados danosos).

Checklist rápido antes de liberar

Encontre riscos de injeção rapidamente
Envie seu código e identificaremos rapidamente as consultas injetáveis e endpoints arriscados.

Antes de considerar pronto, faça uma verificação rápida nos lugares onde a injeção costuma se esconder.

  • Procure no código por texto SQL próximo a valores da requisição. Se vir concatenação de strings ou template strings, trate como inseguro até provar o contrário.
  • Reveja cada cláusula dinâmica, não só o WHERE. ORDER BY, LIMIT/OFFSET e IN (...) são sobras comuns.
  • Confirme que todo valor controlado pelo usuário é vinculado como parâmetro (incluindo entradas numéricas como IDs e tamanhos de página).
  • Garanta que erros não vazem texto SQL ou stack traces.
  • Prove rotas de alto risco (login, busca, filtros administrativos) com alguns payloads básicos e verifique que o comportamento permanece inerte.

Próximos passos: torne o caminho seguro o padrão

A vitória real não é consertar uma consulta. É tornar a injeção difícil de reintroduzir quando uma ferramenta de IA gera um endpoint CRUD “útil”.

Uma regra simples que evita a maioria das regressões: nada de SQL cru construído a partir de strings de requisição. Se precisar de ordenação dinâmica ou seleção de campos, use allowlists estritas.

Se você herdou uma base de código gerada por IA e quer uma revisão rápida e estruturada, FixMyMess (fixmymess.ai) foca em diagnosticar e reparar esses problemas de era de protótipo, incluindo padrões de consulta inseguros, falhas de autenticação e endurecimento de segurança, antes que cheguem à produção.