CVE-2019-16202 (PHP, published 2019-09-10T14:15Z)
Description: value: MISP before 2.4.115 allows privilege escalation in certain situations. After updating to 2.4.115, escalation attempts are blocked by the __checkLoggedActions function with a "This could be an indication of an attempted privilege escalation on older vulnerable versions of MISP (<2.4.115)" message.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-269 \u2014 Improper Privilege Management
The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.
Repository: https://github.com/MISP/MISP
Files changed:
app/Console/Command/AdminShell.php
app/Controller/Component/ACLComponent.php
app/Controller/ServersController.php
app/Controller/UsersController.php
app/Model/Log.php
app/Model/Server.php
app/Model/User.php
app/View/Servers/index.ctp
app/View/Users/admin_index.ctp
Commit message: fix: [security] Fix to a vulnerability related to the server index

- along with various support tools
- more information coming soon


Vulnerable code:
                    'cache' => array('perm_site_admin'),
                    'getSubmodulesStatus' => array('perm_site_admin'),
                    'getSubmoduleQuickUpdateForm' => array('perm_site_admin'),
                    'import' => ('perm_site_admin'),
                    'index' => array('OR' => array('perm_sync', 'perm_admin')),
                    'postTest' => array('perm_sync'),
                    'testConnection' => array('perm_sync'),
        if (!$this->_isSiteAdmin()) {
            if (!$this->userRole['perm_sync'] && !$this->userRole['perm_admin']) {
                $this->redirect(array('controller' => 'events', 'action' => 'index'));
            }
            $this->paginate['conditions'] = array('Server.org_id LIKE' => $this->Auth->user('org_id'));
        }
                    $this->__extralog("change_pw");
                    $this->__extralog("edit", "user", $fieldsResultStr);
            $this->__extralog("delete", $fieldsDescrStr, '');
            $this->__extralog("login");
            $this->__extralog("logout");
    public function resetauthkey($id = null)
            throw new MethodNotAllowedException('Invalid action.');
        $this->User->id = $id;
        if (!$id || !$this->User->exists($id)) {
            throw new MethodNotAllowedException('Invalid user.');
        }
        $user = $this->User->read();
        $oldKey = $this->User->data['User']['authkey'];
        if (!$this->_isSiteAdmin() && !($this->_isAdmin() && $this->Auth->user('org_id') == $this->User->data['User']['org_id']) && ($this->Auth->user('id') != $id)) {
            throw new MethodNotAllowedException('Invalid user.');
        }
        $newkey = $this->User->generateAuthKey();
        $this->User->saveField('authkey', $newkey);
        $this->__extralog(
                'reset_auth_key',
                'Authentication key for user ' . $user['User']['id'] . ' (' . $user['User']['email'] . ')',
                $fieldsResult = 'authkey(' . $oldKey . ') => (' . $newkey . ')'
        );
    private function __extralog($action = null, $description = null, $fieldsResult = null)
    {
        // new data
        $model = 'User';
        $modelId = $this->Auth->user('id');
        if ($action == 'login') {
            $description = "User (" . $this->Auth->user('id') . "): " . $this->data['User']['email'];
        } elseif ($action == 'logout') {
            $description = "User (" . $this->Auth->user('id') . "): " . $this->Auth->user('email');
        } elseif ($action == 'edit') {
            $description = "User (" . $this->User->id . "): " . $this->data['User']['email'];
        } elseif ($action == 'change_pw') {
            $description = "User (" . $this->User->id . "): " . $this->Auth->user('email');
            $fieldsResult = "Password changed.";
        }

        // query
        $this->Log = ClassRegistry::init('Log');
        $this->Log->create();
        $this->Log->save(array(
            'org' => $this->Auth->user('Organisation')['name'],
            'model' => $model,
            'model_id' => $modelId,
            'email' => $this->Auth->user('email'),
            'action' => $action,
            'title' => $description,
            'change' => isset($fieldsResult) ? $fieldsResult : ''));

        // write to syslogd as well
        App::import('Lib', 'SysLog.SysLog');
        $syslog = new SysLog();
        if (isset($fieldsResult) && $fieldsResult) {
            $syslog->write('notice', $description . ' -- ' . $action . ' -- ' . $fieldsResult);
        } else {
            $syslog->write('notice', $description . ' -- ' . $action);
        }
    }

            if ($result === true) {
                $fails[$eventId] = __('Failed (partially?) because of errors: ') . $result;
            $fails[$eventId] = __('failed downloading the event') . ': ' . json_encode($event);

Fixed code:

    public function resetSyncAuthkeys()
    {
        if (empty($this->args[0])) {
            echo sprintf(
                __("MISP mass sync authkey reset command line tool.\n\nUsage: %sConsole/cake resetSyncAuthkeys [user_id]") . "\n\n",
                APP
            );
            die();
        } else {
            $userId = $this->args[0];
            $user = $this->User->getAuthUser($userId);
            if (empty($user)) {
                echo __('Invalid user.') . "\n\n";
            }
            if (!$user['Role']['perm_site_admin']) {
                echo __('User has to be a site admin.') . "\n\n";
            }
            if (!empty($this->args[1])) {
                $jobId = $this->args[1];
            } else {
                $jobId = false;
            }
            $this->User->resetAllSyncAuthKeys($user, $jobId);
        }
    }
                    'cache' => array(),
                    'getSubmodulesStatus' => array(),
                    'getSubmoduleQuickUpdateForm' => array(),
                    'import' => array(),
                    'index' => array(),
                    'postTest' => array(),
                    'testConnection' => array(),
                    'resetAllSyncAuthKeys' => array(),
    private function __checkLoggedActions($user, $controller, $action)
    {
        $loggedActions = array(
            'servers' => array(
                'index' => array(
                    'role' => array(
                        'NOT' => array(
                            'perm_site_admin'
                        )
                    ),
                    'message' => __('This could be an indication of an attempted privilege escalation on older vulnerable versions of MISP (<2.4.115)')
                )
            )
        );
        foreach ($loggedActions as $k => $v) {
            $loggedActions[$k] = array_change_key_case($v);
        }
        $message = '';
        if (!empty($loggedActions[$controller])) {
            if (!empty($loggedActions[$controller][$action])) {
                $message = $loggedActions[$controller][$action]['message'];
                $hit = false;
                if (empty($loggedActions[$controller][$action]['role'])) {
                    $hit = true;
                } else {
                    $role_req = $loggedActions[$controller][$action]['role'];
                    if (empty($role_req['OR']) && empty($role_req['AND']) && empty($role_req['NOT'])) {
                        $role_req = array('OR' => $role_req);
                    }
                    if (!empty($role_req['NOT'])) {
                        foreach ($role_req['NOT'] as $k => $v) {
                            if (!$user['Role'][$v]) {
                                $hit = true;
                                continue;
                            }
                        }
                    }
                    if (!$hit && !empty($role_req['AND'])) {
                        $subhit = true;
                        foreach ($role_req['AND'] as $k => $v) {
                            $subhit = $subhit && $user['Role'][$v];
                        }
                        if ($subhit) {
                            $hit = true;
                        }
                    }
                    if (!$hit && !empty($role_req['OR'])) {
                        foreach ($role_req['OR'] as $k => $v) {
                            if ($user['Role'][$v]) {
                                $hit = true;
                                continue;
                            }
                        }
                    }
                    if ($hit) {
                        $this->Log = ClassRegistry::init('Log');
                        $this->Log->create();
                        $this->Log->save(array(
                                'org' => 'SYSTEM',
                                'model' => 'User',
                                'model_id' => $user['id'],
                                'email' => $user['email'],
                                'action' => 'security',
                                'user_id' => $user['id'],
                                'title' => __('User triggered security alert by attempting to access /%s/%s. Reason why this endpoint is of interest: %s', $controller, $action, $message),
                        ));
                    }
                }
            }
        }
    }

        $this->__checkLoggedActions($user, $controller, $action);

    public function resetRemoteAuthKey($id)
    {
        if (!$this->request->is('post')) {
            throw new MethodNotAllowedException(__('This endpoint expects POST requests.'));
        }
        $result = $this->Server->resetRemoteAuthkey($id);
        if ($result !== true) {
            if (!$this->_isRest()) {
                $this->Flash->error($result);
                $this->redirect(array('action' => 'index'));
            } else {
                return $this->RestResponse->saveFailResponse('Servers', 'resetRemoteAuthKey', $id, $message, $this->response->type());
            }
        } else {
            $message = __('API key updated.');
            if (!$this->_isRest()) {
                $this->Flash->success($message);
                $this->redirect(array('action' => 'index'));
            } else {
                return $this->RestResponse->saveSuccessResponse('Servers', 'resetRemoteAuthKey', $message, $this->response->type());
            }
        }
    }
                    $this->User->extralog($this->Auth->user(), "change_pw", null, null, $user);
                    $user = $this->User->find('first', array(
                        'recursive' => -1,
                        'conditions' => array('User.id' => $this->id)
                    ));
                    $this->User->extralog($this->Auth->user(), "edit", "user", $fieldsResultStr, $user);
            $this->User->extralog($this->Auth->user(), "delete", $fieldsDescrStr, '');
            $this->User->extralog($this->Auth->user(), "login");
            $this->User->extralog($this->Auth->user(), "logout");
    public function resetauthkey($id = null, $alert = false)
            throw new MethodNotAllowedException(__('Invalid action.'));
        }
        $newkey = $this->User->resetauthkey($this->Auth->user(), $id, $alert);
        if ($newkey === false) {
            throw new MethodNotAllowedException(__('Invalid user.'));
    public function resetAllSyncAuthKeys()
    {
        if (!$this->request->is('post')) {
            throw new MethodNotAllowedException(__('This functionality is only accessible via POST requests.'));
        }
        $results = $this->User->resetAllSyncAuthKeysRouter($this->Auth->user());
        if ($results === true) {
            $message = __('Job initiated.');
        } else {
            $message = __('%s authkeys reset, %s could not be reset.', $results['success'], $results['fails']);
        }
        if (!$this->_isRest()) {
            $this->Flash->info($message);
            $this->redirect($this->referer());
        } else {
            return $this->RestResponse->saveSuccessResponse('User', 'resetAllSyncAuthKeys', false, $this->response->type(), $message);
        }
    }

                            'security',
            if ($result) {
                $fails[$eventId] = __('Failed (partially?) because of validation errors: ') . json_encode($eventModel->validationErrors, true);
        ;
            $fails[$eventId] = __('failed downloading the event');

    public function resetRemoteAuthKey($id)
    {
        $server = $this->find('first', array(
            'recursive' => -1,
            'conditions' => array('Server.id' => $id)
        ));
        if (empty($server)) {
            return __('Invalid server');
        }
        $HttpSocket = $this->setupHttpSocket($server);
        $request = $this->setupSyncRequest($server);
        $uri = $server['Server']['url'] . '/users/resetauthkey/me';
        try {
            $response = $HttpSocket->post($uri, '{}', $request);
        } catch (Exception $e) {
            $this->Log = ClassRegistry::init('Log');
            $this->Log->create();
            $message = 'Could not reset the remote authentication key.';
            $this->Log->save(array(
                    'org' => 'SYSTEM',
                    'model' => 'Server',
                    'model_id' => $id,
                    'email' => 'SYSTEM',
                    'action' => 'error',
                    'user_id' => 0,
                    'title' => 'Error: ' . $message,
            ));
            return $message;
        }
        if ($response->isOk()) {
            try {
                $response = json_decode($response->body, true);
            } catch (Exception $e) {
                $this->Log = ClassRegistry::init('Log');
                $this->Log->create();
                $message = 'Invalid response received from the remote instance.';
                $this->Log->save(array(
                        'org' => 'SYSTEM',
                        'model' => 'Server',
                        'model_id' => $id,
                        'email' => 'SYSTEM',
                        'action' => 'error',
                        'user_id' => 0,
                        'title' => 'Error: ' . $message,
                ));
                return $message;
            }
            if (!empty($response['message'])) {
                $authkey = $response['message'];
            }
            if (substr($authkey, 0, 17) === 'Authkey updated: ') {
                $authkey = substr($authkey, 17, 57);
            }
            $server['Server']['authkey'] = $authkey;
            $this->save($server);
            return true;
        } else {
            return __('Could not reset the remote authentication key.');
        }
    }

    public function resetAllSyncAuthKeysRouter($user, $jobId = false)
    {
        if (Configure::read('MISP.background_jobs')) {
            $job = ClassRegistry::init('Job');
            $job->create();
            $eventModel = ClassRegistry::init('Event');
            $data = array(
                    'worker' => $eventModel->__getPrioWorkerIfPossible(),
                    'job_type' => __('reset_all_sync_api_keys'),
                    'job_input' => __('Reseting all API keys'),
                    'status' => 0,
                    'retries' => 0,
                    'org_id' => $user['org_id'],
                    'org' => $user['Organisation']['name'],
                    'message' => 'Issuing new API keys to all sync users.',
            );
            $job->save($data);
            $jobId = $job->id;
            $process_id = CakeResque::enqueue(
                    'prio',
                    'AdminShell',
                    array('resetSyncAuthkeys', $user['id'], $jobId),
                    true
            );
            $job->saveField('process_id', $process_id);
            return true;
        } else {
            return $this->resetAllSyncAuthKeys($user);
        }
    }

    public function resetAllSyncAuthKeys($user, $jobId = false)
    {
        $affected_users = $this->find('all', array(
            'recursive' => -1,
            'contain' => array('Role'),
            'conditions' => array(
                'OR' => array(
                    'Role.perm_sync' => 1,
                    'Role.perm_admin' => 1
                ),
                'Role.perm_site_admin' => 0
            )
        ));
        $results = array('success' => 0, 'fails' => 0);
        $user_count = count($affected_users);
        if ($jobId) {
            $job = ClassRegistry::init('Job');
            $existingJob = $job->find('first', array(
                'conditions' => array('Job.id' => $jobId),
                'recursive' => -1
            ));
            if (empty($existingJob)) {
                $jobId = false;
            }
        }
        foreach ($affected_users as $k => $affected_user) {
            try {
                $reset_result = $this->resetauthkey($user, $affected_user['User']['id'], true);
                if ($reset_result) {
                    $results['success'] += 1;
                } else {
                    $results['fails'] += 1;
                }
            } catch (Exception $e) {
                $results['fails'] += 1;
            }
            if ($jobId) {
                if ($k % 100 == 0) {
                    $job->id =  $jobId;
                    $job->saveField('progress', 100 * (($k + 1) / count($user_count)));
                    $job->saveField('message', __('Reset in progress - %s/%s.', $k, $user_count));
                }
            }
        }
        if ($jobId) {
            $message = __('%s authkeys reset, %s could not be reset', $results['success'], $results['fails']);
            $job->saveField('progress', 100);
            $job->saveField('message', $message);
            $job->saveField('status', 4);
        }
        return $results;
    }

    public function resetauthkey($user, $id, $alert = false)
    {
        $this->id = $id;
        if (!$id || !$this->exists($id)) {
            return false;
        }
        $updatedUser = $this->read();
        $oldKey = $this->data['User']['authkey'];
        if (empty($user['Role']['perm_site_admin']) && !($user['Role']['perm_admin'] && $user['org_id'] == $updatedUser['User']['org_id']) && ($user['id'] != $id)) {
            return false;
        }
        $newkey = $this->generateAuthKey();
        $this->saveField('authkey', $newkey);
        $this->extralog(
                $user,
                'reset_auth_key',
                sprintf(
                    __('Authentication key for user %s (%s) updated.'),
                    $updatedUser['User']['id'],
                    $updatedUser['User']['email']
                ),
                $fieldsResult = 'authkey(' . $oldKey . ') => (' . $newkey . ')',
                $updatedUser
        );
        if ($alert) {
            $baseurl = Configure::read('MISP.external_baseurl');
            if (empty($baseurl)) {
                $baseurl = Configure::read('MISP.baseurl');
            }
            $body = __(
                "Dear user,\n\nan API key reset has been triggered by an administrator for your user account on %s.\n\nYour new API key is: %s\n\nPlease update your server's sync setup to reflect this change.\n\nWe apologise for the inconvenience.",
                $baseurl,
                $newkey
            );
            $bodyNoEnc = __(
                "Dear user,\n\nan API key reset has been triggered by an administrator for your user account on %s.\n\nYour new API key can be retrieved by logging in using this sync user's account.\n\nPlease update your server's sync setup to reflect this change.\n\nWe apologise for the inconvenience.",
                $baseurl,
                $newkey
            );
            $this->sendEmail(
                $updatedUser,
                $body,
                $bodyNoEnc,
                __('API key reset by administrator')
            );
        }
        return $newkey;
    }

    public function extralog($user, $action = null, $description = null, $fieldsResult = null, $modifiedUser = null)
    {
        // new data
        $model = 'User';
        $modelId = $user['id'];
        if (!empty($modifiedUser)) {
            $modelId = $modifiedUser['User']['id'];
        }
        if ($action == 'login') {
            $description = "User (" . $user['id'] . "): " . $user['email'];
        } elseif ($action == 'logout') {
            $description = "User (" . $user['id'] . "): " . $user['email'];
        } elseif ($action == 'edit') {
            $description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
        } elseif ($action == 'change_pw') {
            $description = "User (" . $modifiedUser['User']['id'] . "): " . $modifiedUser['User']['email'];
            $fieldsResult = "Password changed.";
        }

        // query
        $this->Log = ClassRegistry::init('Log');
        $this->Log->create();
        $this->Log->save(array(
            'org' => $user['Organisation']['name'],
            'model' => $model,
            'model_id' => $modelId,
            'email' => $user['email'],
            'action' => $action,
            'title' => $description,
            'change' => isset($fieldsResult) ? $fieldsResult : ''));

        // write to syslogd as well
        App::import('Lib', 'SysLog.SysLog');
        $syslog = new SysLog();
        $syslog->write('notice', $description . ' -- ' . $action . (empty($fieldResult) ? '' : '-- ' . $fieldResult));
    }
            <th><?php echo __('Reset API key');?></th>
        <td id="reset_api_key_<?php echo $server['Server']['id'];?>">
            <?php
                echo $this->Form->postLink(
                    __('Reset'),
                    $baseurl . '/servers/resetRemoteAuthKey/' . $server['Server']['id'],
                    array(
                        'style' => 'line-height:10px; padding: 4px 4px;',
                        'title' => __('Remotely reset API key'),
                        'aria-label' => __('Remotely reset API key'),
                        'class' => 'btn btn-primary'
                    )
                );
            ?>
        </td>
    <?php
        if ($isSiteAdmin) {
            echo sprintf(
                '<span>%s</span>',
                __(
                    'Click %s to reset the API keys of all sync and org admin users in one shot. This will also automatically inform them of their new API keys.',
                    $this->Form->postLink(
                        __('here'),
                        $baseurl . '/users/resetAllSyncAuthKeys',
                        array(
                            'title' => __('Reset all sync user API keys'),
                            'aria-label' => __('Reset all sync user API keys'),
                            'class' => 'bold'
                        ),
                        __('Are you sure you wish to reset the API keys of all users with sync privileges?')
                    )
                )
            );
        }
    ?>