29 dic 2025·4 min de lectura

Inyección SQL en apps CRUD generadas por IA: patrones y soluciones

Aprende a detectar inyección SQL en apps CRUD generadas por IA, con ejemplos concretos de consultas vulnerables y sustituciones seguras usando parámetros y características del ORM.

Inyección SQL en apps CRUD generadas por IA: patrones y soluciones

Cómo se ve la inyección SQL en apps CRUD

La inyección SQL ocurre cuando una aplicación permite que la entrada del usuario cambie el significado de una consulta a la base de datos. En vez de tratarse como datos, la entrada se convierte en parte del SQL. Eso puede exponer datos privados, modificar registros o borrar tablas.

Los endpoints CRUD son objetivos comunes porque reciben entradas constantemente: cuadros de búsqueda, filtros, formularios de edición, IDs en URLs y paneles de administración. Los atacantes no necesitan nada sofisticado. Solo necesitan un lugar donde la entrada se cosiga en una consulta.

En muchos prototipos generados por IA, los puntos riesgosos parecen “normales” a primera vista. Los generadores de código suelen recurrir a la concatenación rápida de cadenas, especialmente alrededor de la búsqueda, los filtros y el ordenamiento. También tienden a juntar el parseo de la petición, la construcción de la consulta y el formato de la respuesta en una sola función larga, lo que hace que una línea insegura sea fácil de pasar por alto.

Señales de que una ruta CRUD podría ser inyectable:

  • Cadenas SQL construidas con +, plantillas o reemplazos de cadena usando valores de la petición
  • Texto de búsqueda insertado directamente en LIKE '%...%'
  • ORDER BY dinámico o nombres de columna tomados de parámetros de consulta
  • Endpoints de administración que confían en las entradas porque son “internos”
  • Errores que muestran SQL crudo o detalles de la base de datos

Una regla simple funciona bien: si la base de datos puede interpretar la entrada del usuario como palabras clave SQL (como OR, UNION, DROP), no estás realmente parcheado. Si la base de datos solo ve la entrada del usuario como valores ligados a parámetros, vas en la dirección segura.

Patrones vulnerables comunes que producen las herramientas de IA

La inyección suele aparecer en los mismos lugares: búsqueda, filtrado, ordenación y paginación. Estas áreas parecen manejo inocuo de cadenas, pero tocan la base de datos en casi cada petición.

Un patrón clásico es concatenar la entrada del usuario en una cláusula WHERE:

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

Otro patrón común es construir “filtros opcionales” agregando fragmentos crudos en un bucle, especialmente LIKE '%${q}%' y status = '${status}'. El riesgo crece rápido en cuanto se omite un campo o se “sanitiza” con un replace débil.

El ordenamiento y la paginación son sobrantes frecuentes. La gente busca inyección en WHERE, y luego olvida que ORDER BY y LIMIT a menudo vienen directamente de los parámetros:

// 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);

También verás atajos repetidos en prototipos:

  • Escapar en algunos lugares pero olvidarlo en un endpoint
  • Permitir nombres de columna arbitrarios para un “ordenamiento flexible”
  • Tratar los IDs como seguros porque “son números”, y luego usarlos como cadenas
  • Registrar SQL crudo con entrada de usuario (lo que puede filtrar datos sensibles)
  • Copiar y pegar un patrón de consulta inseguro en muchas rutas

Si heredaste una app generada por IA con herramientas como Bolt, v0, Cursor o Replit, asume que estos patrones existen hasta que demuestres lo contrario.

Ejemplo concreto: arreglando SQL crudo con parámetros

La interpolación de cadenas suele verse limpia, pero mezcla código y entrada de usuario en la misma cadena.

Vulnerable:

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

Si alguien pasa x' OR '1'='1, la consulta puede devolver todos los usuarios. La solución es mantener el texto SQL estático y pasar los valores por separado.

Reemplazo seguro: placeholders + valores separados

Placeholders en PostgreSQL:

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

Placeholders en MySQL/SQLite:

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

La clave es simple: la entrada no se pega en la cadena SQL. El driver la envía como dato, no como código.

Casos límite: listas IN (...) y valores vacíos

Los filtros como “status in [a, b, c]” a menudo se parchean mal.

Inseguro:

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

Más seguro: construir placeholders y seguir pasando valores por separado.

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

if (statuses.length === 0) return res.json([]); // or skip the filter

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

Reglas prácticas que evitan regresiones desordenadas:

  • Trata las cadenas vacías como “sin filtro”, no como texto SQL
  • Valida tipos temprano (los números deben ser números antes de consultar)
  • Nunca aceptes fragmentos SQL crudos desde la petición, ni siquiera los que parecen “inocuos”
  • Mantén la construcción de consultas en un solo lugar para que las correcciones se apliquen de forma consistente

Ejemplo concreto: filtrado seguro sin SQL construido en cadena

Un patrón de inyección común es convertir “filtros opcionales” en una cadena WHERE en crecimiento.

Forma vulnerable:

// ❌ 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}`;

Patrón más seguro: construye condiciones por separado, mantiene la entrada fuera del texto SQL y pasa los 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);

Algunos detalles que evitan errores sutiles:

  • Pon los comodines % en el parámetro, no en la cadena SQL
  • Parsear y validar fechas, luego vincularlas como parámetros
  • Convertir rangos numéricos y rechazar NaN antes de vincular
  • Si un filtro es opcional, omite la condición por completo

Ejemplo concreto: ordenación y paginación seguras

Endurece tu capa de base de datos
Obtén endurecimiento de seguridad con verificación humana para que las correcciones perduren más allá de un endpoint.

La ordenación es donde endpoints que son “parametrizados” a veces se vuelven riesgosos. Valores como el texto de búsqueda pueden parametrizarse. Nombres de columna y direcciones de orden no pueden.

Patrón seguro: mapear la entrada del usuario a una lista de permitidos pequeña de columnas y direcciones, y rechazar todo lo demás.

// 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]
  };
}

Flujo de trabajo paso a paso para parchear una app CRUD existente

Cuando una app CRUD “funciona en su mayoría”, la inyección a menudo se esconde en los bordes: búsqueda, filtros, orden, paneles admin y acciones masivas. Un flujo de parche evita que arregles un endpoint y dejes tres abiertos.

  1. Inventaría cada consulta y ruta de entrada. Lista endpoint, tipo de consulta (lectura/escritura), de dónde vienen las entradas y qué campos llegan a la base de datos. Incluye jobs en background y herramientas de administración.

  2. Reemplaza SQL construido con cadenas por parámetros. Busca concatenaciones de consultas y cámbialas por consultas parametrizadas o un generador de consultas. Haz esto incluso para endpoints “internos”.

  3. Agrega listas de permitidos para identificadores. No puedes parametrizar de forma segura nombres de columna, tablas o direcciones de orden. Si la entrada controla ORDER BY, columnas seleccionadas o joins, mapea las entradas a identificadores seguros conocidos.

  4. Añade algunas pruebas focalizadas. Envía payloads que intenten romper el escape (una comilla simple), trucos booleanos comunes (OR 1=1) y tipos inesperados. Asegura comportamiento seguro: no filas extra, sin filtrado de datos, sin errores SQL expuestos.

  5. Revisa logs y manejo de errores. Provoca errores a propósito y confirma que las respuestas no incluyan SQL crudo, stack traces o detalles del driver. Mantén errores detallados en logs del servidor y redacta valores que puedan contener datos sensibles.

Uso más seguro de ORM (y trampas a evitar)

Un ORM puede bloquear la inyección, pero solo si te mantienes en sus rutas seguras. Eso normalmente significa dejar que el ORM construya el SQL mientras pasas la entrada del usuario como valores.

Los patrones “seguros” se ven como “filtrar por estos campos”, no como “construir una cadena 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);

Los ORM aún tienen salidas peligrosas, y el código generado por IA a menudo las usa en exceso. Ten cuidado con:

  • Helpers de consultas crudas que usan interpolación de cadenas
  • Métodos explícitamente etiquetados como “unsafe”
  • Pasar entrada de usuario en identificadores (nombres de columna, tablas)
  • APIs que aceptan un fragmento SQL en lugar de un valor

Si debes usar SQL crudo, usa la característica de parámetros del ORM, no las plantillas de cadena. Para nombres de columna dinámicos, usa una whitelist.

Errores comunes al parchear la inyección SQL

Audita cada camino de consulta
Revisamos búsquedas, filtros, ordenación y rutas de administración donde suele esconderse la inyección SQL.

La validación de entrada no es suficiente. Bloquear unos pocos caracteres o recortar espacios puede reducir ruido, pero no impide la inyección si un valor todavía termina dentro de una cadena SQL.

El escapado manual es otra trampa. Se siente más seguro reemplazar comillas, pero las reglas de escape difieren entre bases de datos y los casos borde son fáciles de pasar por alto. Las consultas parametrizadas son más seguras y más fáciles de revisar.

Las declaraciones preparadas también pueden usarse mal. Un error frecuente es parametrizar el valor de búsqueda pero seguir concatenando piezas SQL como ORDER BY:

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

Si sort o dir vienen de la petición, un atacante puede salir del contexto. Arréglalo con una whitelist estricta para identificadores.

El registro (logging) es un riesgo subestimado también. Registrar SQL completo más la entrada del usuario puede filtrar correos, tokens u otros secretos copiados en una caja de búsqueda. Mantén los logs a un nivel alto y enmascara campos sensibles.

Ejemplo realista: endpoint de búsqueda en admin vulnerable

Un fundador despliega un panel admin hecho por IA con prisa. Tiene una caja de búsqueda simple: “Buscar usuarios por email o nombre.” El backend construye una cadena SQL con lo que el admin escriba y la ejecuta tal cual.

Parece inofensivo porque es “solo admin”. Pero los endpoints admin se exponen en el mundo real: una ruta mal configurada, una cookie filtrada, una contraseña débil o una herramienta interna desplegada por error en una URL pública.

La inyección ocurre cuando la entrada de búsqueda se coloca dentro de una consulta como:

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

Un payload realista cierra la comilla y añade una condición verdadera, como %' OR 1=1 --. Ahora la cláusula WHERE siempre coincide y la respuesta puede volcar muchos más datos de los previstos.

En una versión parcheada, la misma búsqueda usa parámetros. Si alguien envía %' OR 1=1 --, se trata como texto plano y se comporta como una búsqueda normal (habitualmente sin resultados relevantes).

Lista rápida antes de lanzar

Triage de problemas de seguridad
Si sospechas secretos expuestos o consultas descuidadas, haremos el triaje y priorizaremos las correcciones.

Antes de dar por concluido, haz un repaso rápido por los lugares donde la inyección aún se esconde.

  • Busca en el código texto SQL cerca de valores de petición. Si ves concatenación o plantillas, trátalo como inseguro hasta demostrar lo contrario.
  • Revisa cada cláusula dinámica, no solo WHERE. ORDER BY, LIMIT/OFFSET e IN (...) son residuos comunes.
  • Confirma que cada valor controlado por el usuario está ligado como parámetro (incluyendo inputs numéricos como IDs y tamaños de página).
  • Asegúrate de que los errores no filtren texto SQL o stack traces.
  • Prueba rutas de alto riesgo (login, búsqueda, filtros admin) con unos pocos payloads básicos y verifica que el comportamiento permanezca inerte.

Siguientes pasos: hacer que la ruta segura sea la predeterminada

La verdadera victoria no es parchear una consulta. Es hacer que la inyección sea difícil de reintroducir la próxima vez que una herramienta de IA genere un endpoint “útil”.

Una regla simple que evita la mayoría de regresiones: no construir SQL crudo a partir de cadenas de petición. Si necesitas ordenación dinámica o selección de campos, usa listas de permitidos estrictas.

Si heredaste una base de código generada por IA y quieres una revisión rápida y estructurada, FixMyMess (fixmymess.ai) se centra en diagnosticar y reparar estos problemas de era prototipo, incluyendo patrones de consulta inseguros, ruptura de autenticación y endurecimiento de seguridad, antes de que lleguen a producción.