La mayoría de herramientas de reconocimiento de bug bounty paran en el mismo sitio. Subdominios encontrados. Hosts vivos sondeados. Nuclei lanzado. Una lista de URLs y una carpeta de archivos .js descargados. Y ahí el humano se va a dormir, porque rebuscar dentro de un bundle de webpack de 3 MB a las 11 de la noche no le apetece a nadie.
Los hallazgos que importan viven pasado ese punto. Hace un tiempo escribí por qué no te conviene saltarte los archivos JavaScript. Este es el cómo, una vez el archivo está abierto.
Diez minutos es la ventana que le doy a un bundle nuevo antes de decidir si el objetivo merece tiempo a fondo. No es mucho, pero da para tirar de los tres o cuatro hilos que te dicen si hay algo aquí. El orden de abajo es el que uso. Los primeros pasos te dan palanca sobre los siguientes, así que trabaja de arriba abajo salvo que alguno sea un callejón sin salida.
Primero, siempre, busca un source map. Si el bundle ha salido a producción con su source map, los nueve minutos siguientes son fáciles.
grep -oE '//# sourceMappingURL=.*' app.abc123.js
# → //# sourceMappingURL=app.abc123.js.map
curl -s https://target.example.com/static/app.abc123.js.map -o app.mapUn source map de verdad te da el árbol de archivos original. Componentes sin minificar, definiciones de rutas con nombres reales de funciones, comentarios que el desarrollador dejó mientras construía la feature. Herramientas tipo sourcemapper o un unwebpack-sourcemap rápido convierten el .map en un directorio de proyecto que puedes grepear como si fuera código normal. Un bundle con source map es otro nivel de esfuerzo comparado con uno sin él.
Si no ves la referencia, no pares. Que falte sourceMappingURL no significa que no haya mapa. A veces está en la ruta por defecto, pegado al bundle. Prueba app.abc123.js.map directamente.
Segundo, saca la lista de endpoints. Antes de leer una línea de código, saber con qué superficies habla el frontend.
grep -oE '"/api/[^"]+"' app.abc123.js | sort -u
grep -oE '(?:fetch|axios|request)\s*\([^)]+\)' app.abc123.js
grep -oE 'baseURL["'\'']?\s*[:=]\s*["'\''][^"'\'']+' app.abc123.jsLa primera línea saca las rutas literales. La segunda las llamadas en tiempo de ejecución donde la ruta puede estar compuesta a partir de constantes. La tercera encuentra las URLs base que prefijan a todo lo demás. Haces la unión, deduplicas y tienes la superficie REST de la aplicación en una lista.
Dos cosas a las que prestar atención al repasarla. Una, cualquier ruta que termine en /internal/, /_admin/, /dev/, /debug/, /v0/, /legacy/. Dos, cualquier ruta a la que la navegación de la aplicación no te lleva nunca. Las dos son señales de que el endpoint está ahí pero nadie lo anuncia.
Tercero, saca las cadenas de roles y permisos. El frontend casi siempre trae el modelo de control de acceso en forma de strings, porque los condicionales de UI se escriben tipo if (user.role === 'admin').
grep -oE '["'\''](role|roles|permissions?|scope|can_|has_|is_)[^"'\'']{0,40}["'\'']' app.abc123.js | sort -u
grep -oE '["'\''](admin|superuser|manager|owner|internal|support)["'\'']' app.abc123.js | sort -uLo que sale es la lista de roles que conoce el backend y las comprobaciones de capacidad que el frontend usa para decidir qué enseñar. Ya sabes cómo es el modelo de acceso visto desde fuera. Si alguno de esos roles o capacidades no aparecía en tu flujo normal de alta de cuenta, es un rol que el servidor sigue respetando y que la UI no te deja escoger. Registra, inspecciona tu token, cambia un claim, prueba el endpoint privilegiado.
Cuarto, feature flags y ramas por entorno. Cualquier frontend que no sea trivial tiene flags.
grep -oE 'process\.env\.[A-Z_]+' app.abc123.js | sort -u
grep -oE '(isProd|isDev|isInternal|DEBUG|FEATURE_[A-Z_]+|FLAG_[A-Z_]+)' app.abc123.js | sort -uLos flags te dicen dos cosas. Una, qué funcionalidades construyó el equipo y luego tapó. Dos, cómo decide el frontend qué rama ejecuta. Un parámetro de URL, una cookie, una cabecera, un endpoint de configuración. Si consigues invertir la condición que el frontend comprueba, la funcionalidad es alcanzable. ?debug=1, localStorage.setItem('isInternal', 'true'), una cabecera que la API acepta como override. Todo eso ha llegado a producción en algún sitio.
Quinto, identificadores hardcodeados. Cadenas que no deberían estar dentro de un bundle.
# JWTs
grep -oE 'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}' app.abc123.js
# AWS keys, Stripe, GitHub PAT, GitLab token
grep -oE '(AKIA|sk_live_|pk_live_|ghp_|glpat-)[A-Za-z0-9]{16,}' app.abc123.js
# Bloques largos base64/hex
grep -oE '[A-Za-z0-9+/]{40,}={0,2}' app.abc123.js | head -20La mayoría van a ser falsos positivos. URLs con data: en CSS, hashes de chunks de webpack, subsets de fuentes. Filtra a saco. Los de verdad son cortos y están fuera de lugar. Un JWT dentro de un bundle es un JWT que alguien commiteó en vez de pedirlo por fetch. Una clave Stripe en live es una clave en live. Un PAT de GitHub son credenciales.
A los desarrolladores les dicen que no pueden commitear estas cosas. Las commitean igual. Con la frecuencia suficiente como para que los regex de arriba paguen lo que cuesta correrlos en casi cualquier objetivo.
Sexto, mensajes de error y comentarios. El bundle minifica identificadores pero no literales de string, porque las strings son lo que la app le enseña al usuario. Los mensajes de error sobreviven.
grep -oE '"[^"]{20,120}"' app.abc123.js | grep -iE 'error|fail|forbid|denied|unauth|invalid|token'
grep -oE '/\*[!*][^*]{10,}\*/' app.abc123.js
grep -oE '//[!*][^\n]{10,}' app.abc123.jsRascar mensajes de error saca a la luz funcionalidades a las que la UI no te deja llegar. Un texto tipo "Cannot delete payment method: user is not the account owner" te dice que hay una comprobación de autorización sobre esa acción. En otro punto del bundle está el código que llama a esa acción. Encuéntralo. Ya tienes un endpoint con nombre y una regla de autorización concreta que probar.
Los comentarios de licencia (/*! jQuery ... */) te los puedes saltar. Lo que interesa es TODO, FIXME, HACK, @deprecated, nombres de autor, referencias a tickets. Esos sobreviven a la minificación cuando el autor usó la marca de "comentario preservado" (/*! ... */). A veces incluyen el problema exacto que el desarrollador sabía que tenía y nunca arregló.
Si correr seis greps sobre cada archivo JS de cada objetivo te suena repetitivo, es que lo es. Hay herramientas que empaquetan el patrón en una sola pasada. jsluice (de BishopFox, en Go, basado en tree-sitter) saca endpoints, secretos y URLs del bundle en una ejecución. LinkFinder (en Python) es la opción clásica para extraer endpoints. trufflehog es el mejor escáner de secretos de propósito general y funciona perfecto sobre archivos .js, no solo sobre repos git. sourcemapper reconstruye el árbol de proyecto original cuando hay source map, así que el paso uno se convierte en un solo comando. Úsalas como primera pasada. Lo que se les escapa es lo que un humano leyendo el bundle pilla sin esfuerzo: una cadena de permiso que no está en su lista de patrones, un flag con un nombre raro, un mensaje de error que insinúa la forma de un endpoint. La automatización es un filtro, no un sustituto. Si el escaneo automático vuelve vacío y el bundle es grande, léelo igualmente.
A los diez minutos tendrías que tener: una lista de endpoints a grandes rasgos, un modelo de roles, una lista de flags, tal vez una o dos credenciales filtradas y un puñado de notas de tipo "interesante pero aún no es un bug". Eso no son hallazgos. Es un mapa.
Las dos horas siguientes es donde salen hallazgos y aquí va la parte honesta. La mayoría de objetivos no dan más de sí pasados los diez minutos. La mayoría tienen bundles limpios, sin source map, sin roles ocultos, sin claves filtradas. Vas a gastar más ventanas de diez minutos que reportes vas a escribir. La disciplina no es tratar los diez minutos como un examen que apruebas. Es tratarlos como triaje y pasar de largo sin culpa cuando el bundle no tiene nada que contar.
Los bundles que sí tienen algo que contar suelen soltar varias de estas señales a la vez, no una. Un source map más roles ocultos más un flag que la URL deja activar es un día entero de pruebas y normalmente un High. Un TODO suelto es curiosidad.
Una cosa más. Los regex de arriba funcionan sobre el bundle literal. Cuando consigues un source map, vuelve a correrlos sobre el proyecto desempaquetado. La densidad de señal sube por un factor de diez, porque estás grepeando código original, no la salida del minificador. Si tienes source map, el orden de prioridad cambia. Source map primero, luego los pasos dos a seis sobre el árbol desempaquetado.
Todo lo que viene después, probar los endpoints, confirmar el bypass de autorización, encadenar hacia algo que paguen, es el mismo trabajo que en cualquier otro objetivo. La parte que cambia es llegar a la lista de cosas que vale la pena probar en diez minutos en vez de en una tarde. Eso es lo que te da leer el bundle.
