Reglas
Las reglas toman los valores que controla el cliente en la petición (ruta, query, cuerpo, cabeceras y cookies, ya decodificados) y revisan si alguno cambia el significado de la operación que se va a ejecutar.
Fuera de una petición HTTP, como en un cron o al arrancar, no se revisa nada.
sql: inyección SQL
Intercepta las consultas de pg, mysql2, better-sqlite3 y supabase-js. En los tres primeros separa el SQL en tokens según el dialecto. Si un valor del cliente queda entero dentro de un literal, se trata como dato. Si abarca varios tokens, se marca como inyección.
db.query(`SELECT * FROM t WHERE a = '${v}'`);
v | Resultado |
|---|---|
Ana | Pasa |
select * from users | Pasa: es un dato dentro del literal |
' OR 1=1 -- | Bloquea |
x'; DROP TABLE t; -- | Bloquea |
También contempla la sintaxis de cada motor: el \ como escape de MySQL, los $$ de Postgres, y los comentarios ejecutables /*! */ de MySQL.
supabase-js. Las consultas se envían por HTTP a PostgREST, y Vantor revisa la petición antes del envío. La regla se aplica a .or() y select, donde las comas y los paréntesis son parte de la sintaxis:
supabase.from('perfiles').select('id').or(`nombre.ilike.%${q}%`);
q | Resultado |
|---|---|
María José | Pasa |
x%,rol.eq.admin | Bloquea: agregó una condición |
x),org_id.neq.1,(a.eq.b | Bloquea: cerró el grupo |
Si el valor va entre comillas dobles (`nombre.ilike."%${q}%"`), una coma se trata como dato y una comilla se bloquea. Los filtros como .eq('nombre', q) envían un único valor y no tienen este riesgo.
supabase-js devuelve el error como { error } en lugar de lanzarlo. Si tu código hace if (error) throw, la petición termina en 500 sin llegar a la base. Esta regla no reemplaza las políticas RLS. Con la service key, RLS no se aplica, por lo que cada consulta debe verificar la pertenencia del recurso.
Otros drivers. Prisma, por ejemplo, no se puede interceptar. Envolvé la consulta a mano:
import { guardSql } from 'vantor-shield';
await prisma.$queryRawUnsafe(guardSql(`SELECT * FROM t WHERE a = '${v}'`, { dialect: 'pg' }));
Las consultas tipadas de Prisma y el $queryRaw con template no lo necesitan, porque envían los valores como parámetros.
command: inyección de comandos
Revisa exec, execSync, y spawn, execFile y sus variantes síncronas. Con shell, analiza si el valor puede salirse de sus comillas, según las reglas de POSIX o de cmd.exe. Sin shell, solo se revisa el nombre del programa.
| Código | v | Resultado |
|---|---|---|
exec(`ping ${v}`) | 8.8.8.8 | Pasa |
exec(`ping ${v}`) | 8.8.8.8; cat /etc/passwd | Bloquea |
exec(`ping ${v}`) | $(whoami) | Bloquea |
exec(`echo '${v}'`) | hola; mundo | Pasa: las comillas simples lo contienen |
spawn('ping', [v]) | ; rm -rf / | Pasa: sin shell es solo un argumento |
path: path traversal
Revisa las funciones de fs y fs/promises, en sus formas callback, síncrona, promesa y stream. Detecta segmentos ../ del cliente aunque path.join() ya los haya resuelto, rutas absolutas enviadas por el cliente y bytes nulos.
| Código | v | Resultado |
|---|---|---|
readFile(join(base, v)) | informe.pdf | Pasa |
readFile(join(base, v)) | ../../etc/passwd | Bloquea |
readFile(v) | /etc/passwd | Bloquea |
Un archivo llamado informe..final.pdf pasa, porque .. no es un segmento de ruta.
ssrf: falsificación de peticiones del servidor
Revisa http, https y fetch. Bloquea solo si el cliente eligió el destino y ese destino es interno (localhost, redes privadas, servicio de metadatos de la nube).
Las llamadas de la aplicación a sus servicios internos no se bloquean, porque el host no viene del cliente.
También cubre:
- Direcciones ofuscadas:
2130706433,0x7f.0.0.1y[::ffff:7f00:1]se reconocen como127.0.0.1. - DNS rebinding: con
httpyhttpsrevisa la IP a la que se va a conectar. - Redirecciones: un host público que redirige a
169.254.169.254se bloquea antes de seguir la redirección.
Con fetch, la revisión de DNS ocurre justo antes de conectar y no durante la conexión, por lo que queda una ventana breve para rebinding. Ver Límites.
Para permitir un destino interno elegido por el cliente, usá ssrfAllowHosts.
input: entradas peligrosas
Rechaza antes de que el valor llegue a tu código:
- Prototype pollution: las claves
__proto__yconstructor.prototype. - Operadores de consulta:
$ne,$gt,$wherey similares, donde se esperaba un valor, como{"password": {"$ne": null}}en MongoDB. - Cuerpos no inspeccionables: un cuerpo de texto mayor a
maxInspectBytes, con más de 50.000 valores, o con más de 60 niveles de anidación. Se reportan y, en modo bloqueo, se rechazan. Los archivos binarios y multipart no entran en esta regla.
Si tu API recibe JSON grande, como imágenes en base64, subí maxInspectBytes por encima del límite de tu parser. Los data URLs en base64 válidos no se tratan como entradas.
ratelimit
Token bucket por cliente, en memoria. Cuando se excede responde 429 con Retry-After.
{
"trustProxy": 1,
"rateLimit": {
"max": 300,
"windowMs": 60000,
"routes": [{ "path": "/login", "max": 10, "methods": ["POST"] }]
}
}
Está desactivado por defecto. Detrás de un proxy sin trustProxy, el límite se aplicaría a todos los usuarios juntos. Se aplica por proceso, y cada instancia lleva su propia cuenta.
Cabeceras
En modo bloqueo agrega, salvo que las desactives:
| Cabecera | Valor |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | SAMEORIGIN |
Referrer-Policy | strict-origin-when-cross-origin |
Cross-Origin-Opener-Policy | same-origin-allow-popups |
Content-Security-Policy | base-uri 'self'; object-src 'none'; frame-ancestors 'self' |
Strict-Transport-Security | max-age=15552000, solo en HTTPS |
También quita X-Powered-By. Estos valores están elegidos por compatibilidad con aplicaciones existentes. La CSP por defecto no restringe scripts ni frena XSS. Para restringirlos, activá el nonce:
protect({ mode: 'block', headers: { csp: { nonce: true } } });
import { cspNonce } from 'vantor-shield';
res.send(`<script nonce="${cspNonce()}">...</script>`);
Si tu código define una de estas cabeceras, se usa la de tu código.