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}'`);
vResultado
AnaPasa
select * from usersPasa: 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}%`);
qResultado
María JoséPasa
x%,rol.eq.adminBloquea: agregó una condición
x),org_id.neq.1,(a.eq.bBloquea: 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ódigovResultado
exec(`ping ${v}`)8.8.8.8Pasa
exec(`ping ${v}`)8.8.8.8; cat /etc/passwdBloquea
exec(`ping ${v}`)$(whoami)Bloquea
exec(`echo '${v}'`)hola; mundoPasa: 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ódigovResultado
readFile(join(base, v))informe.pdfPasa
readFile(join(base, v))../../etc/passwdBloquea
readFile(v)/etc/passwdBloquea

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:

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:

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:

CabeceraValor
X-Content-Type-Optionsnosniff
X-Frame-OptionsSAMEORIGIN
Referrer-Policystrict-origin-when-cross-origin
Cross-Origin-Opener-Policysame-origin-allow-popups
Content-Security-Policybase-uri 'self'; object-src 'none'; frame-ancestors 'self'
Strict-Transport-Securitymax-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.