Playground de Client-Side Path Traversal
Playground en un solo puerto con 3 mini-apps independientes. C1 (fácil) introduce el concepto base: la SPA concatena un parámetro de URL en un `fetch` sin sanitizar y un `../` lo redirige a otro endpoint. C2 (medio) añade un bot admin Puppeteer y un bypass de validación con URL encoding + `decodeURIComponent` tardío. C3 (difícil) encadena un allowlist con `%`, un path collapse `..` y el truco de `?` para mantener intacto el body POST mientras se redirige el fetch a `/api/audit/flag-export` con cookie auditor. La flag se entrega únicamente cuando la cadena del C3 se completa via bot. Estado en memoria, gating progresivo server-side, código vulnerable accesible al alumno desde el propio navegador
Learn to find this bug
A real bug bounty report, reproduced for you to practice.
Create your account and practice real bugs that got paid. Download the environment, find it and learn the exact technique — your path to your first bounty.
- hunters training
- 650
- labs from real reports
- 50
- completions
- 380
- in bounties practiced
- $200,000
hunters training
labs from real reports
completions
in bounties practiced
Access to all labs · no commitment · cancel anytime
Hunters who solved it· 6
Objectives
Achievement you'll earn
Solve this lab to unlock this shareable achievement
Playground de Client-Side Path Traversal
Community writeups
CSPT-VULN es un playground autocontenido. Un solo proceso Node.js + Express
en el puerto 1301 sirve 3 sub-aplicaciones independientes bajo/challenge-1, /challenge-2, /challenge-3, más un hub en / que muestra
el estado de las 3 (🔒 / 🔓 / ✅) y un writeup interactivo en /writeup.
El bug es siempre el mismo: el frontend construye una URL para unfetch(...) concatenando un valor que viene de la query string sin
sanitizar. El alumno controla ese valor; el navegador normaliza la ruta;
la request llega al servidor apuntando a otro endpoint que ofrece más
privilegios o más información.
Capa 1 — La Biblioteca de Atlas (CWE-22 / Easy)
Catálogo de libros por categoría. Vista vulnerable (challenge-1.js):
const categoria = new URLSearchParams(location.search).get('categoria') || 'ficcion';
fetch('/api/v1/library/' + categoria);
Payload: ?categoria=../admin-vault. Browser construye/api/v1/library/../admin-vault, normaliza a /api/v1/admin-vault. El
endpoint sensible devuelve { unlock: true } y, como la sesión es la del
usuario, el backend marca c1 = true. Sin bot — el fetch lo hace el propio
navegador del alumno.
Capa 2 — Soporte Atlas (CWE-22 + CWE-602 / Medium)
Sistema de tickets. La página /challenge-2/view carga challenge-2.js,
que lee ?ticket= en su forma cruda con un regex, valida que no haya/ ni .. literales, decodifica y concatena:
const ticketRaw = location.search.match(/[?&]ticket=([^&]+)/)?.[1];
if (ticketRaw.indexOf('/') !== -1 || ticketRaw.indexOf('..') !== -1) reject();
const ticketId = decodeURIComponent(ticketRaw);
fetch('/api/tickets/' + ticketId);
Payload encoded: ticket=%2E%2E%2Fadmin%2Fusers%2Fusr_ATK_xxxx%2Fpromote.
La validación pasa (no hay / ni .. literales en el raw). El decode
produce ../admin/users/usr_ATK_xxxx/promote. Browser normaliza,/api/tickets/../admin/users/usr_ATK_xxxx/promote →/api/admin/users/usr_ATK_xxxx/promote. Como el bot admin es quien abrió
la URL, su cookie autoriza la promoción. El backend resuelve "qué atacante
disparó este bot" via botManager.getActiveJobOwner() y marca su c2.
Pista pública: GET /api/admin/_self/audit-policy lista los endpoints
documentados para el rol support-bot — entre ellos/api/admin/users/:userId/promote.
Capa 3 — Auditoría Cumplimiento (CWE-22 + CWE-602 + CWE-918 / Hard)
Panel interno donde el bot auditor revisa casos con un operator_token
firmado por boot. Vista vulnerable (challenge-3.js):
const refRaw = location.search.match(/[?&]ref=([^&]+)/)?.[1];
if (!/^[A-Za-z0-9._%\-]+$/.test(refRaw)) reject(); // allowlist permite %
const ref = decodeURIComponent(refRaw);
fetch('/api/audit/case/' + ref + '/review', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
operator_token: window.__AUDITOR_TOKEN, // inyectado solo si cookie = bot auditor
decision: 'approve',
notes: 'Reviewed via dashboard',
}),
});
Payload: ref=%2E%2E%2Fflag-export%3F. Allowlist pasa (todos los chars son
alfanuméricos + . + %). Decode → ../flag-export?. Fetch URL string =/api/audit/case/../flag-export?/review. El URL parser del navegador:
- Detecta
?→ cambia a query state. Path queda en/api/audit/case/../flag-export. - Normalización:
..consumecase/. Path final:/api/audit/flag-export. - Query final:
/review.
El POST viaja a /api/audit/flag-export con el body intacto (incluyendo eloperator_token que el servidor inyectó en window.__AUDITOR_TOKEN solo
porque la cookie es la del bot auditor). El backend valida, devuelve la
flag, y la acredita al ownerSessionId del job activo en botManager.
Sesiones, bots y crédito
- Sesión humana: cookie
cspt_sid = <random 16 bytes>, emitida en el
primerGET /o/api/progress. - Sesión bot admin:
cspt_sid = bot-admin-<random>, inyectada por
Puppeteer antes de cada navegación del bot admin. - Sesión bot auditor:
cspt_sid = bot-auditor-<random>, equivalente. - Cuando llega una petición con cookie de bot, el handler consulta
botManager.getActiveJobOwner()para saber qué sesión humana disparó
al bot — y le acredita el resultado (promotion, flag).
Por qué este patrón es real
Reportes públicos relevantes que inspiran el lab:
- Slack Workspace Takeover via CSPT (HackerOne disclosed): el cliente
construía paths con input del usuario y un admin bot seguía links
internos. - GitLab CSPT (CVE-2021-22165): construcción client-side de paths en
un widget de notificaciones. - Medusa CSPT (PortSwigger research, 2024): patrón de validación sobre
forma raw + decode tardío en plataformas e-commerce.
El lab condensa el patrón en tres formas progresivas y entrena al alumno
para reconocerlo en código JS real.
Mitigaciones (cubiertas en el writeup)
- Validar después de decodificar, no antes.
- Allowlist sin
%si el segmento no necesita escapes. - Usar
new URL(...)conencodeURIComponent(segment)por cada segmento,
no concatenación de strings. - Aislar tokens privilegiados:
window.__AUDITOR_TOKENno debería estar
expuesto en una página que acepta input de query string. - Defense in depth server-side: el endpoint
/api/audit/flag-export
podría exigir un HMAC del path completo + body, validable solo si el path
es el esperado. - Allowlist del bot por path, no solo por host: rechazar URLs que
contengan caracteres sensibles como..,%2E%2E,?en sufijo.
Flag
FLAG{a29ee59e1fd23ecc5a4d2d53880a6866}
Verificación:
echo -n 'cspt-vuln::client-side-path-traversal::final-2026' | md5sum
# a29ee59e1fd23ecc5a4d2d53880a6866
O simplemente bash flag/verify.sh.