When Auth Key Reset Skips the Privilege Check: A MISP Access Control Bug
A look at how an incomplete fix from 2019 left MISP's auth key reset endpoint vulnerable to privilege escalation, and what the patch looks like.
MISP has a hierarchical privilege model: regular users, organization administrators, and site administrators. Org admins manage users within their own organization, but should never be able to affect accounts with site-level privileges.
While auditing MISP's access control, I noticed that this boundary was not enforced on the authentication key reset endpoint. An organization administrator could reset the API key of a site administrator in the same organization and receive the newly generated key in the response.
This has happened before
In 2019, CVE-2019-12794 addressed the same conceptual issue: org admins could reset passwords for site admin accounts. The fix added a perm_site_admin check to the password reset path.
The authentication key reset path, however, is a separate function. It never received the equivalent protection. The same privilege escalation vector remained exploitable through a different endpoint for seven years. (See also the related SQL injection I reported at the same time.)
The vulnerable code
The resetauthkey() function in app/Model/User.php performed the following authorization check:
public function resetauthkey($user, $id, $alert = false, $keyId = null)
{
$this->id = $id;
if (!$id || !$this->exists($id)) {
return false;
}
$updatedUser = $this->read();
if (empty($user['Role']['perm_site_admin'])
&& !($user['Role']['perm_admin']
&& $user['org_id'] == $updatedUser['User']['org_id'])
&& ($user['id'] != $id)) {
return false;
}
// proceeds to generate and return new auth key
}
The logic asks three questions about the caller:
- Are they a site admin? If yes, allow.
- Are they an org admin in the same organization as the target? If yes, allow.
- Are they resetting their own key? If yes, allow.
What's missing is any check on the target's privilege level. If a site admin account exists in the same organization as an org admin, condition #2 passes and the key reset proceeds without restriction.
There's a compounding problem: $this->read() loaded the target user without joining the Role association. So even if a developer had tried to check $updatedUser['Role']['perm_site_admin'], that field wouldn't exist in the returned data.
The listing layer
The same gap existed in AuthKeysController::__prepareConditions(), which controlled visibility of auth keys across the application:
private function __prepareConditions()
{
$user = $this->Auth->user();
if ($user['Role']['perm_site_admin']) {
$conditions = [];
} else if ($user['Role']['perm_admin']) {
$conditions['AND'][]['User.org_id'] = $user['org_id'];
} else {
$conditions['AND'][]['User.id'] = $user['id'];
}
return $conditions;
}
This method was shared by index(), view(), delete(), edit(), and pin(). Org admins had full visibility into auth key metadata (including partial key values) for site admin accounts in their organization.
Exploitation
The attack is a single API call:
curl -sk https://TARGET/users/resetauthkey/1 \
-H "Authorization: <org_admin_api_key>" \
-H "Accept: application/json" \
-X POST
Response:
{
"saved": true,
"name": "Authkey updated: <new_site_admin_key>"
}
The org admin now holds a valid site admin API key. The previous key is invalidated, locking out the legitimate site admin. From here, the attacker has full control over the MISP instance: user management, server configuration, sync settings, and access to all threat intelligence data across every organization.
Why this bypassed existing protections
Every other user-management action in MISP uses a helper called __adminFetchConditions() (in UsersController.php), which adds Role.perm_site_admin = false to database queries. This prevents org admins from interacting with site admin accounts. The resetauthkey path was the only user management function that did not use this helper.
The ACL layer (ACLComponent.php) defines access for this endpoint as:
'resetauthkey' => ['AND' => ['self_management_enabled', 'perm_auth', 'not_read_only_authkey']]
This controller-level check validates that the caller has auth permissions, but the actual cross-user authorization was delegated to the model layer. And that's where the check was missing.
The fix
Both issues were fixed in commit cb40488 by Andras Iklody.
In User.php, the target user is now loaded with the Role association, and a privilege check blocks the operation when a non-site-admin targets a site-admin account:
$updatedUser = $this->find('first', array(
'conditions' => ['User.id' => $id],
'recursive' => -1,
'contain' => ['Role']
));
if (empty($updatedUser)) {
return false;
}
if (empty($user['Role']['perm_site_admin'])
&& !empty($updatedUser['Role']['perm_site_admin'])) {
return false;
}
In AuthKeysController.php, the conditions query now excludes users with site admin roles entirely:
} else if ($user['Role']['perm_admin']) {
$conditions['AND'][]['User.org_id'] = $user['org_id'];
$siteAdminRoleIds = $this->AuthKey->User->Role->find('list', [
'recursive' => -1,
'fields' => ['Role.id', 'Role.id'],
'conditions' => ['Role.perm_site_admin' => true],
]);
if (!empty($siteAdminRoleIds)) {
$conditions['AND'][]['User.role_id NOT IN'] = array_keys($siteAdminRoleIds);
}
}
Site admin auth keys are now invisible to org admins across all AuthKeysController actions.
The broader picture
This vulnerability is a good example of what happens when security fixes are applied too narrowly. CVE-2019-12794 fixed the password reset path. The auth key reset path, which does functionally the same thing (generates a new credential and returns it), was not covered.
If you're auditing access control in a similar application, it helps to think in terms of "what are all the ways a credential can be created or modified?" and verify each one independently. Checking the caller's privilege level is necessary but not sufficient. You need to check the target's too.