Cómo hacer un pentest de una API REST con curl

Sat Aug 15 2026

La mayoría de las guías de pentest de APIs dividen la lista de OWASP en once tareas no relacionadas. Pero ese orden omite las comprobaciones más rápidas. Una segunda identidad y JSON sin procesar suelen revelar fallos de autorización antes que la búsqueda de payloads.

Y prueba únicamente una API de laboratorio en staging o deliberadamente vulnerable, con permiso por escrito. Necesitas dos identidades de prueba con diferente propiedad o roles, tokens válidos, Bash con curl y jq, y un límite de solicitudes y una condición de detención acordados. Burp Suite es útil para la interceptación y la repetición; OWASP ZAP es una opción gratuita razonable para escaneos de referencia automatizados.

Comienza con una API de laboratorio en https://api.shop.test. El fixture proporciona a Alice el pedido 1001 y a Bob el pedido 1002. Usa datos de prueba equivalentes en tu entorno. Descubriremos rutas, repetiremos objetos entre identidades, inspeccionaremos tokens y campos, mediremos límites, probaremos flujos de negocio y rastrearemos URLs controladas por el usuario. OWASP API Security Top 10 sigue siendo la edición de 2023 a fecha del 15 de agosto de 2026; úsala para etiquetar los hallazgos mientras el orden de la prueba prioriza primero las fronteras de identidad y autorización.

En este artículo

El alcance es un control, no una exención de responsabilidad

Registra los hosts objetivo y las rutas excluidas. Anota las cuentas de prueba y las reglas de gestión de datos. Añade el horario de pruebas, los límites de tasa de solicitudes y la persona de contacto si el servicio se comporta mal. Decide de antemano qué detiene una prueba: aumento de las tasas de error, crecimiento de la cola, exposición inesperada de datos o cualquier tráfico de producción.

Configura el harness. La variable del proxy envía el tráfico de línea de comandos a través de Burp u otro interceptor aprobado. Importa su CA únicamente de acuerdo con la configuración aprobada de tu laboratorio.

export HTTPS_PROXY=http://127.0.0.1:8080

BASE=https://api.shop.test
TOKEN_A='alice-test-token'
TOKEN_B='bob-test-token'

Los valores siguientes representan un objetivo autorizado. OWASP’s API Security Project proporciona el marco de riesgos; no sustituye la evidencia de solicitudes y respuestas. Comienza con BOLA, broken authentication, BOPLA y BFLA. Esos controles establecen quién puede acceder a un objeto, convertirse en una identidad, cambiar una propiedad o llamar a una función.

Construye el inventario de rutas antes de probar los controles

La lista de rutas de la SPA es una pista para el descubrimiento, no un límite de cobertura. Comprueba las especificaciones comunes y los metadatos de identidad. ¿Qué revela la API?

for p in swagger-ui api-docs v3/api-docs openapi.json \
  .well-known/openid-configuration; do
  curl -s -o /dev/null -w "%{http_code} $p\n" "$BASE/$p"
done

Si el documento OpenAPI está protegido, repite con un token de prueba aprobado. Un 401 significa “se requiere autenticación”. La documentación aún puede estar presente.

curl -fsS "$BASE/openapi.json" |
  jq -er 'if (.paths | type) == "object" then .paths | keys[] else error("no object paths") end' \
  > endpoints.txt

Aquí -f rechaza los errores HTTP y -e hace que un resultado nulo provoque un fallo. Un documento malformado ya hace que jq falle.

Para un frontend de staging accesible, extrae URLs que parezcan de API desde JavaScript:

katana -u https://app.shop.test -jc -silent |
  grep -E '/api|/v[0-9]' |
  sort -u > discovered-urls.txt

Mantén las URLs completas separadas de las rutas relativas de OpenAPI. Herramientas como nuclei necesitan URLs accesibles:

nuclei -l discovered-urls.txt -t http/exposures/

Sondea versiones antiguas usando el prefijo que realmente utiliza tu API:

for v in v1 v2 v3 internal beta; do
  curl -s -o /dev/null -w "%{http_code} /api/$v/users\n" \
    "$BASE/api/$v/users"
done

Una respuesta de documentación accesible ayuda a encontrar pistas. No determina su gravedad. Registra por separado la documentación expuesta, las rutas obsoletas, los stack traces y los secretos del lado del cliente. Restringe la documentación de producción, retira las versiones antiguas, suprime los traces y elimina las claves distribuidas.

Si el inventario revela /api/orders/{id}, comienza por ahí.

Dos identidades exponen la autorización de objetos

Repite un objeto con la identidad incorrecta

BOLA es la demostración útil más rápida. Mantén constantes el método, la forma de la ruta, las cabeceras y el token mientras cambias únicamente el ID del objeto. Después invierte ambas identidades.

Alice lee su pedido:

curl -s "$BASE/api/orders/1001" \
  -H "Authorization: Bearer $TOKEN_A" |
  jq .

Ahora Alice solicita el pedido de Bob:

curl -i -s "$BASE/api/orders/1002" \
  -H "Authorization: Bearer $TOKEN_A"

Un 403 o un 404 que no divulgue información puede ser correcto. La propiedad y los datos devueltos importan más que el estado por sí solo. Una respuesta que contenga el pedido de Bob, o un cambio de estado en él, constituye el hallazgo.

Repite en la otra dirección:

curl -i -s "$BASE/api/orders/1001" \
  -H "Authorization: Bearer $TOKEN_B"

Los UUID hacen que los identificadores sean más difíciles de adivinar, pero no aplican la autorización. Prueba identificadores en rutas, cadenas de consulta, cuerpos JSON, cabeceras, recursos anidados, arrays por lotes, exportaciones, descargas y todos los métodos compatibles. Puede existir una comprobación de lectura mientras la actualización o eliminación sigue abierta.

Repite entre ubicaciones y métodos

Usa ffuf únicamente con IDs de prueba conocidos y una tasa de solicitudes baja:

ffuf -u "$BASE/api/orders/FUZZ" \
  -w <(printf '%s\n' 1001 1002) \
  -H "Authorization: Bearer $TOKEN_A" \
  -mc 200,403,404 \
  -rate 1

Un resultado positivo de un scanner aquí casi no tiene valor. Guarda la solicitud original y la repetición. Registra en qué se diferencian las respuestas. Indica el límite de identidad afectado y cualquier cambio de estado. Esa es la evidencia.

Trata los tokens como entradas, no como hechos

Un JWT legible demuestra la codificación. No dice nada sobre la verificación de la firma ni la validación de claims.

Este decoder gestiona el padding común de base64url:

JWT="$TOKEN_A"

python3 - "$JWT" <<'PY'
import base64, json, sys

for part in sys.argv[1].split(".")[:2]:
    part += "=" * (-len(part) % 4)
    print(json.dumps(
        json.loads(base64.urlsafe_b64decode(part)),
        indent=2
    ))
PY

Inspecciona alg, kid, iss, aud, sub, los roles y la expiración. La repetición manual de claims es la prueba principal. Para versiones compatibles con estos modos, jwt_tool puede generar variantes:

jwt_tool --help
jwt_tool "$JWT" -T
jwt_tool "$JWT" -X a

Una variante generada es únicamente un artefacto de prueba. Informa sobre confusión de algoritmos o aceptación de alg:none solo si el servidor acepta el token y cambia el acceso.

En un laboratorio que use tokens emitidos para pruebas y claves de prueba, puedes comprobar secretos HMAC débiles:

jwt_tool "$JWT" -C -d /usr/share/wordlists/rockyou.txt

Para una prueba de referencia de clave, usa únicamente una fuente de claves de laboratorio aprobada:

jwt_tool "$JWT" -X k -pk ./lab-issuer-public.pem

Comprueba payloads alterados, firmas no válidas, tokens caducados, aud o iss incorrectos, repetición tras logout y reutilización del refresh token. La presencia de kid, jku o x5u no es un hallazgo por sí misma. Prueba si esas referencias permanecen restringidas a claves de confianza y mantén las solicitudes dentro del laboratorio.

La solución comienza con una allowlist de algoritmos y la validación de la firma y los claims. Restringe las referencias de claves. Rota los refresh tokens, revócalos y detecta su reutilización. El punto débil de este método es el comportamiento asíncrono: curl puede mostrar un enqueue exitoso mientras el trabajo peligroso ocurre después. Confirma esos casos con logs o con el responsable; no infieras el impacto a partir de la respuesta HTTP.

Compara los campos devueltos con los campos modificables

BOPLA cubre lecturas y escrituras:

DirecciónEvidencia concretaSolución
Exposición de lecturaUna respuesta apropiada para un rol incluye un campo que debería omitirDevuelve un DTO de respuesta explícito
Overposting de escrituraUn GET posterior muestra que isAdmin, la propiedad u otro valor protegido ha cambiadoAplica una allowlist de campos modificables en el servidor

Inspecciona la respuesta completa de Alice, ocultando los secretos:

curl -s "$BASE/api/users/me" \
  -H "Authorization: Bearer $TOKEN_A" |
  jq .

Prueba un campo cada vez contra datos desechables. Restablece la cuenta del laboratorio después de cada mutación:

curl -i -s -X PATCH "$BASE/api/users/me" \
  -H "Authorization: Bearer $TOKEN_A" \
  -H "Content-Type: application/json" \
  -d '{"verified":true}'

Solo cuando el entorno de prueba lo permita, intenta con campos protegidos como role, isAdmin, ownerId o balance. Un campo reflejado es una pista. Una lectura posterior que muestre un privilegio o una propiedad cambiados es evidencia.

Arjun puede sugerir parámetros JSON, aunque su gestión del body varía según la versión:

arjun -u "$BASE/api/users/me" \
  -m JSON \
  -H "Authorization: Bearer $TOKEN_A"

Mide los límites y prueba la identidad en la que confían

Un resultado positivo de un scanner aquí casi no tiene valor. Rate limiting es una cuestión de identidad y control de costes, no un recuento procedente de una única dirección de origen.

Envía una prueba de login limitada:

for i in $(seq 1 20); do
  curl -s -o /dev/null -w "$i %{http_code}\n" \
    -X POST "$BASE/api/login" \
    -H "Content-Type: application/json" \
    -d "{\"user\":\"alice-test\",\"pass\":\"wrong-$i\"}"
  sleep 0.2
done

curl -s -D - -o /dev/null \
  -X POST "$BASE/api/login" \
  -H "Content-Type: application/json" \
  -d '{"user":"alice-test","pass":"wrong-final"}'

Cuando las solicitudes 1–8 devuelvan 401 y la solicitud 9 devuelva 429, registra una ventana de ocho intentos. Repite después del reset y registra Retry-After, si está presente.

Mantén constantes el token, la ruta, el payload, el tiempo y el número de solicitudes mientras comparas la línea base con las cabeceras reenviadas:

for mode in baseline rotated; do
  printf '%s: ' "$mode"
  for i in $(seq 1 5); do
    args=()
    [ "$mode" = rotated ] &&
      args=(-H "X-Forwarded-For: 192.0.2.$i")
    curl -s -o /dev/null -w "%{http_code} " \
      "$BASE/api/search?q=test" "${args[@]}"
  done
  printf '\n'
done

Un mayor número de solicitudes exitosas al rotar sugiere que una cabecera falsificable influye en el límite. No demuestra por sí solo un bypass.

Comprueba la paginación y las rutas costosas con solicitudes pequeñas:

for q in 'limit=9999999' 'per_page=100000' 'page_size=-1'; do
  curl -s "$BASE/api/items?$q" -o /dev/null \
    -w "$q status=%{http_code} bytes=%{size_download}\n"
done

Prueba exportaciones, envíos de OTP, rutas por lotes y la profundidad o el batching de GraphQL únicamente cuando estén dentro del alcance. Detente en el límite acordado.

Para el cupón sin cargo de la API de laboratorio, prueba el estado en lugar de la carga:

for i in $(seq 1 3); do
  curl -s -X POST "$BASE/api/apply-coupon" \
    -H "Authorization: Bearer $TOKEN_A" \
    -H "Content-Type: application/json" \
    -d '{"code":"SAVE50"}'
  printf '\n'
done

La primera aplicación debería modificar el carrito de prueba. Los intentos posteriores deberían rechazarse o dejarlo sin cambios.

Separa el acceso a funciones del abuso de flujos de negocio

El token habitual de Alice no debería acceder a datos administrativos:

curl -i -s "$BASE/api/admin/users" \
  -H "Authorization: Bearer $TOKEN_A"

Prueba los métodos documentados, las versiones, las rutas bulk y cualquier comportamiento de method override:

curl -i -s -X POST "$BASE/api/orders/1001" \
  -H "Authorization: Bearer $TOKEN_A" \
  -H "X-HTTP-Method-Override: DELETE"

Compara la respuesta y el estado con una solicitud normal contra datos desechables. Cada método y versión necesita autorización default-deny.

Después prueba los flujos sensibles con recuentos bajos. Repite la solicitud del cupón anterior y, si el inventario contiene controles de referidos o cantidades, pruébalos con cuentas desechables. Para /api/checkout, utiliza únicamente productos sin cargo y un número pequeño de solicitudes acordado. Un estado exitoso importa cuando demuestra una transición de estado no autorizada. También puede mostrar un descuento duplicado o una infracción de cantidad.

Sigue los valores hasta los posibles sinks

Tratar la inyección como una única cadena de SQLi es una forma perezosa de probar. Primero encuentra dónde termina cada valor controlado por el usuario; después usa probes inofensivos y confirma el sink.

EntradaSink hipotéticoProbe seguroEvidencia
sort o filterParser de base de datos o búsquedaCambiar entre valores documentadosReflexión, error del parser o cambio controlado del resultado
Campo similar a una plantillaRenderizador de plantillasEnviar un marcador inerte únicoRenderizado inesperado en el servidor
Campo JSON o XMLParser o servicio posteriorAñadir un campo desconocido benignoComportamiento del parser o del servicio posterior
url o callback_urlCliente HTTP del servidorEnviar una URL de callback aprobadaMetadatos de la solicitud out-of-band

El endpoint /api/fetch del laboratorio acepta una URL:

curl -i -s -X POST "$BASE/api/fetch" \
  -H "Authorization: Bearer $TOKEN_A" \
  -H "Content-Type: application/json" \
  -d '{"url":"<YOUR_CONTROLLED_CALLBACK_URL>"}'

Un fetch ciego puede no devolver un body útil; el observador de callbacks proporciona la evidencia. Registra la marca de tiempo, la dirección de origen, el método y la ruta.

Después prueba un servicio interno controlado:

curl -i -s -X POST "$BASE/api/fetch" \
  -H "Authorization: Bearer $TOKEN_A" \
  -H "Content-Type: application/json" \
  -d '{"url":"http://127.0.0.1:8080/health"}'

Una respuesta del servicio del laboratorio sugiere claramente que la ruta de fetch lo alcanzó. Confirma el origen con los logs del servicio o con los datos del callback. Usa representaciones alternativas del loopback únicamente contra la red de staging controlada, nunca contra direcciones internas de producción. No solicites credenciales de metadata de la nube; demostrar la alcanzabilidad interna controlada es suficiente para establecer la ruta SSRF.

Si el inventario contiene source_url, link, image_url o webhook, aplica el mismo proceso. Para las hipótesis de SQL, NoSQL, command, template y parser, registra la reflexión, los errores, el tiempo y el comportamiento posterior. No afirmes que existe un sink basándote únicamente en un error extraño.

Informa sobre la solicitud, la respuesta y el cambio de estado

Lista de comprobación del hallazgo

Usa las etiquetas de 2023. BOPLA es A03, unrestricted resource consumption es A04, sensitive business flows es A06 y SSRF es A07. La clasificación de OWASP no determina por sí sola la gravedad.

Antes de cerrar la evaluación, verifica:

  • endpoint y método
  • identidad y rol utilizados
  • solicitud original y repetida
  • respuesta y diferencia relevante del JSON
  • datos o estado afectados
  • ID de OWASP y corrección
  • condición de retest

Una checklist proporciona cobertura. El trío solicitud/respuesta/estado te proporciona un hallazgo.