Possible site takeover through stolen API credentials in combination with SQLi – (WPUmbrella <= 2.10.0)

Affected pluginWPUmbrella
Active installs10,000+
Vulnerable version<= 2.10.0
Audited version2.10.0
Fully patched version2.11.0
Recommended remediationImmediately update the plugin to version 2.11.0 or higher


WPUmbrella’s remote application uses a local companion plugin to perform its functionality.

The communication between the remote WPUmbrella application and the WordPress site is secured using a shared secret stored as plaintext in the WordPress options table.

An attacker that can read the plaintext value can fully impersonate WPUmbrella’s remote application and perform all actions, including logging in as an administrator.

The plaintext value could be obtained, for example, through:

Proof of concept

The plugin uses the ApiWordPressPermission class for request authentication.

The isSecretTokenAuthorized method accepts the provided token from the HTTP request and compares it to the token stored in the local database.

The local token is obtained by calling wp_umbrella_get_secret_token, which ultimately calls get_option and returns the database value as is.

The API keys are stored as plaintext in the “wp-health” option.

class ApiWordPressPermission
    public function isSecretTokenAuthorized($userSecretToken, $options = [])
        if (!$userSecretToken || empty($userSecretToken)) {
            return ['authorized' => false, 'code' => 'api_key_empty', 'message' => 'API Key is empty'];

        $withCache = $options['with_cache'] ?? true;
        $secretTokenSave = wp_umbrella_get_secret_token();
        if ((!$secretTokenSave || empty($secretTokenSave)) && defined('WP_UMBRELLA_SECRET_TOKEN')) {
            $secretTokenSave = WP_UMBRELLA_SECRET_TOKEN;

        if (!$secretTokenSave && !$withCache) {
            $secretTokenSave = wp_umbrella_get_service('Option')->getSecretTokenWithoutCache();

        if (!hash_equals($secretTokenSave, $userSecretToken)) {
            return ['authorized' => false, 'code' => 'not_authorized', 'message' => 'API Key not authorize'];

        return ['authorized' => true];
public function getOptions($params = [])
    $secure = $params['secure'] ?? true;

    $options = wp_parse_args(get_option(WP_UMBRELLA_SLUG), $this->getOptionsDefault());

    if($secure) {


    return $options;

Proposed patch

Security-sensitive data, such as API keys, can not be stored as plaintext in the WordPress database.

There are two possible approaches to fix this issue.

1. Storing a hash of the API key:

// On Storage:
$key = sodium_crypto_generichash(wp_salt('auth'));
$store_me = sodium_crypto_generichash($api_key, $key);

// On Validation:
$stored_value = /** GET OPTION FROM DATABASE */

$key = sodium_crypto_generichash(wp_salt('auth'));
$provided_value = sodium_crypto_generichash($api_key_from_request, $key);

$valid = hash_equals($stored_value, $provided_value);

A keyed hash even protects against an attacker with full-write access to the option/database since the key is not stored there.

Note: Using wp_salt() could result in hash mismatches if users rotate their wp-config salts. A custom constant would be better.

2. Using asymmetric cryptography

Instead of relying on a shared hash, the communication could be secured with asymmetric cryptography.

WPUmbrella’s remote application would hold the private key and the WordPress site’s corresponding public key.

The remote application then signs its HTTP requests using the private key, which the WordPress site can authenticate using the public key.

However, an attacker could only compromise the public key, which on its own, is useless.


Vendor contactedJuly 03, 2023
First ResponseJuly 03, 2023
Fully patched atJuly 06, 2023
Publicly disclosedJuly 06, 2023


WPUmbrella has shown exceptional cooperation. It’s evident that the company takes security issues very seriously.

Leave a Reply

Your email address will not be published. Required fields are marked *