SQL injection y consultas parametrizadas en PostgreSQL | Nicolás Garzón
La parametrización separa valores de la estructura SQL. Protege strings, números y fechas, pero no convierte nombres de tabla, columnas, operadores ni direcciones de orden en parámetros.
TypeScript
Copiar await client. query (
'SELECT id, name FROM products WHERE business_id = $1 AND name = $2' ,
[ businessId, name] ,
) ; El servidor recibe SQL y valores por canales separados. El input no puede cerrar una comilla y convertirse en código.
TypeScript
Copiar const sql = ` SELECT * FROM users WHERE email = ' ${ email} ' ` ; modifica la estructura. Escapar manualmente no es una estrategia confiable.
Strings.
Números.
Booleanos.
Dates/timestamps.
UUID.
Arrays como valores.
JSON.
No puede parametrizarse directamente:
Nombre de columna.
Tabla/schema.
ASC/DESC.
Operador.
Keyword.
Fragmento SQL.
TypeScript
Copiar const columns = {
createdAt: 'created_at' ,
total: 'total_amount' ,
} as const ;
const directions = { asc: 'ASC' , desc: 'DESC' } as const ;
const column = columns[ input. sortBy] ;
const direction = directions[ input. direction] ;
if ( ! column || ! direction) throw new Error ( 'Invalid sort' ) ;
const sql = `
SELECT id, created_at, total_amount
FROM orders
WHERE business_id = $1
ORDER BY ${ column} ${ direction} , id ${ direction}
LIMIT $2
` ; Solo se interpolan fragmentos definidos por el código.
TypeScript
Copiar ` WHERE id IN ( ${ ids. join ( ',' ) } ) ` SQL
Copiar WHERE id = ANY ( $1 ::bigint [ ] ) Define semántica para array vacío.
Parámetros previenen SQL injection, pero % y _ siguen siendo wildcards del lenguaje LIKE:
SQL
Copiar WHERE name ILIKE '%' || $1 || '%' Si buscas texto literal, escapa esos caracteres según un ESCAPE definido. Parametrización y escaping de patrones resuelven problemas distintos.
Aunque sean parámetros, el usuario todavía controla un pequeño lenguaje que puede ser costoso o producir errores. Prefiere constructores seguros:
websearch_to_tsquery frente a to_tsquery libre.
Regex limitada en longitud/timeout.
JSONPath generado por código cuando sea complejo.
La ausencia de injection no garantiza resistencia a abuso de recursos.
SQL
Copiar EXECUTE format ( 'SELECT count(*) FROM %I.%I' , p_schema, p_table)
INTO result; SQL
Copiar EXECUTE 'SELECT * FROM app.orders WHERE id = $1'
USING p_id; %I cita identifiers, USING parametriza valores. Aun así, valida que el identifier pertenezca a un conjunto permitido.
Reducen concatenación accidental, pero no eliminan injection:
APIs raw.
Nombre de columna dinámico.
Filtros construidos como strings.
Migraciones y scripts.
orderByRaw.
Revisa el SQL generado y limita escape hatches.
Input peligroso puede almacenarse como dato y más tarde concatenarse en otro SQL administrativo. La validación al ingresar no sustituye parametrizar en cada punto de ejecución.
Si ocurre injection, el impacto depende del role:
Texto
Copiar runtime sin DDL ni acceso a otras schemas
→ daño limitado
runtime owner/superuser
→ compromiso amplioDefense in depth: parámetros + allowlists + roles mínimos + timeouts + auditoría.
No devuelvas SQL, stack, schema o detalles de constraints al cliente no confiable. Registra contexto seguro internamente con query normalizada, no passwords ni valores sensibles.
Algunos drivers desactivan múltiples statements o utilizan extended protocol. No dependas de eso como defensa principal. Una condición inyectada dentro de un único SELECT ya puede exfiltrar información.
Parámetro NULL necesita cast para inferir tipo.
Valores bigint/numeric pueden llegar como strings.
Identifiers case-sensitive requieren quoting coherente.
Límite/offset deben validarse aunque se parametrizen para evitar DoS.
Sort dinámico puede filtrar información por timing si permite expresiones arbitrarias.
Escapar comillas a mano.
Concatenar arrays.
Creer que ORM vuelve seguro todo raw SQL.
Parametrizar una columna con $1.
Validar solo frontend.
Role con permisos excesivos.
Confundir input válido con input barato.
Comillas y comentarios SQL.
Unicode y backslashes.
Arrays vacíos/grandes.
Sort inválido.
Regex/FTS costosa.
NULL y casts.
APIs raw del ORM.
Privilegios del runtime.
Mensajes de error.
Timeouts.
Valores son parámetros; estructura es whitelist.
Escapar patrones no es igual a evitar injection.
Query builders también tienen escape hatches.
Input almacenado puede atacar más tarde.
Privilegios mínimos limitan impacto.
Seguridad incluye coste y errores, no solo sintaxis.
¿Por qué ORDER BY $1 no elige columna?
¿Cómo consultas una lista segura?
¿Qué diferencia hay entre parametrizar y escapar LIKE?
¿Por qué un ORM no basta?
¿Qué es second-order injection?
Ver respuestas
$1 representa un valor constante, no un identifier.
Con = ANY($1::type[]) o placeholders generados de forma segura.
Parametrizar protege SQL; escapar LIKE cambia wildcards del patrón.
Las APIs raw y estructura dinámica todavía pueden concatenar código.
Dato almacenado que luego se inserta inseguramente en otra sentencia.
Conexiones, sesiones y pooling administra el recurso más limitado entre aplicación y base.