CVE-2026-54834

Object Cache 4 everyone <= 2.3.2 - Information Exposure

mediumExposure of Sensitive Information to an Unauthorized Actor
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
2.3.3
Patched in
9d
Time to patch

Description

The Object Cache 4 everyone plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 2.3.2. This makes it possible for unauthenticated attackers to extract sensitive user or configuration data.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Low
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=2.3.2
PublishedJune 17, 2026
Last updatedJune 25, 2026

What Changed in the Fix

Changes introduced in v2.3.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan focuses on **CVE-2026-54834**, an information exposure vulnerability in the "Object Cache 4 everyone" plugin. The vulnerability arises from storing serialized cache data in web-accessible files with predictable names and locations when using the disk-based caching backend. --- #…

Show full research plan

This research plan focuses on CVE-2026-54834, an information exposure vulnerability in the "Object Cache 4 everyone" plugin. The vulnerability arises from storing serialized cache data in web-accessible files with predictable names and locations when using the disk-based caching backend.


1. Vulnerability Summary

The Object Cache 4 everyone plugin (<= 2.3.2) provides a disk-based fallback when Memcached is unavailable. This disk backend stores cached objects (which often include sensitive site configuration, user metadata, and session transients) as serialized PHP data. These files are stored within the wp-content/cache/object/ directory.

Because the directory structure and filenames are derived from a simple MD5 hash of the cache key, and the files use a .object.php extension without proper access controls (like .htaccess or placement outside the web root), an unauthenticated attacker can predict the path to sensitive cache entries and download them directly via HTTP.

2. Attack Vector Analysis

  • Endpoint: Direct HTTP GET requests to files located under /wp-content/cache/object/.
  • Vulnerable File: object-cache-disk-template.php (specifically the _get_path and set methods).
  • Payload: No payload is required for the request itself; the "attack" is the prediction of the file path.
  • Authentication: None (Unauthenticated).
  • Preconditions:
    1. The plugin must be active.
    2. The disk cache must be in use (Memcached must be unavailable or disabled via OC4EVERYONE_DISABLE_DISK_CACHE).
    3. The web server must allow access to the wp-content/cache/ directory.

3. Code Flow

The vulnerability can be traced through the following call stack:

  1. Entry: A WordPress process calls wp_cache_set( $key, $value, $group ).
  2. Processing: The WP_Object_Cache class (installed by the plugin) formats the internal key, typically using the pattern $blog_prefix . $group . ':' . $key.
  3. Sink (Write): ObjectCacheDisk::set($key, $value) is called in object-cache-disk-template.php.
  4. Path Generation: ObjectCacheDisk::_get_path($key) (line 157) calculates the file path:
    $hash = md5($key); // Returns 32-character hex string
    $array_hash = str_split($hash, 8); // Splits into four 8-character segments
    $path = $this->local_path . implode(DIRECTORY_SEPARATOR, $array_hash); // wp-content/cache/object/S1/S2/S3/S4/
    // ...
    $path .= DIRECTORY_SEPARATOR . '.object.php'; // Final filename: .object.php
    
  5. Storage: The serialized data is written to the file via $wp_filesystem->put_contents($path, @serialize($value)).
  6. Exposure: The attacker requests the URL corresponding to the generated $path.

4. Nonce Acquisition Strategy

No nonce is required. This vulnerability involves direct access to static files on the filesystem. WordPress nonces are not checked by the web server when serving .php or other files from the wp-content/cache directory.

5. Exploitation Strategy

The goal is to retrieve the alloptions cache entry, which contains a massive amount of site configuration data.

  1. Identify Target Key: The standard key for site options is options:alloptions. In a default single-site WordPress installation (Blog ID 1), the prefixed key used by the cache engine is 1:options:alloptions.
  2. Calculate Path:
    • Target Key: 1:options:alloptions
    • MD5 Hash: 0043818e100f95470d046c879d72a5a5
    • Split segments: 0043818e, 100f9547, 0d046c87, 9d72a5a5
    • Predicted Path: /wp-content/cache/object/0043818e/100f9547/0d046c87/9d72a5a5/.object.php
  3. Execute Request: Use the http_request tool to fetch the file.
  4. Parse Data: The response will be a serialized PHP string. Use a script or unserialize() to extract the site configuration.

6. Test Data Setup

To verify the vulnerability in a test environment:

  1. Environment: A standard WordPress installation with the plugin "Object Cache 4 everyone" <= 2.3.2.
  2. Trigger Fallback: Ensure no Memcached server is running on the local host to force the plugin to use the ObjectCacheDisk backend.
  3. Populate Cache: Log in as an administrator and browse the dashboard. This ensures the alloptions and users:1 keys are cached to disk.
  4. Confirm Storage: Check that the directory wp-content/cache/object/ has been created and populated with subdirectories.

7. Expected Results

  • An HTTP GET request to the predicted path returns a 200 OK status.
  • The response body starts with serialized PHP markers (e.g., a:NNN:{...} or O:8:"stdClass":...).
  • The data contains sensitive information such as the admin_email, siteurl, active_plugins, and potentially database schema information or transient secrets.

8. Verification Steps

  1. Verify File Existence: Use WP-CLI to confirm the path the plugin should be using:
    wp eval "echo WP_CONTENT_DIR . '/cache/object/' . implode('/', str_split(md5('1:options:alloptions'), 8)) . '/.object.php';"
  2. Check Content: Compare the output of the HTTP request with the actual option value:
    wp option get alloptions --format=json
  3. Check Direct Access: Ensure that attempting to access the directory itself (/wp-content/cache/object/) returns a 403 or an empty index.php, but the specific .object.php file remains accessible.

9. Alternative Approaches

If 1:options:alloptions does not exist, the plugin or WordPress configuration might not be using a blog prefix. Try these variations:

  • No Prefix: options:alloptions
    • MD5: 53066d9341498b5e9036c57f2023d8c2
    • Path: /wp-content/cache/object/53066d93/41498b5e/9036c57f/2023d8c2/.object.php
  • User Data: 1:users:1 (Admin User Data)
    • MD5: 7f26798e8334465439a38f36c5339c0f
    • Path: /wp-content/cache/object/7f26798e/83344654/39a38f36/c5339c0f/.object.php
  • Custom Salt: If the site defines WP_CACHE_KEY_SALT in wp-config.php, the key will be SALT + blog_prefix + group + : + key. The agent would need to discover this salt (e.g., via a different leak) to predict the MD5.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Object Cache 4 everyone plugin (<= 2.3.2) for WordPress stores cached data in predictable locations on the filesystem using MD5 hashes of cache keys. Due to a lack of directory access controls and the use of a .php extension, unauthenticated attackers can directly access and download these files to extract sensitive site configuration or user information.

Vulnerable Code

// object-cache-disk-template.php line 106
    public function set($key, $value, $expiration = 0)
    {
        global $wp_filesystem;
        //Find file and put
        $path = $this->_get_path($key);

        //Folder for file
        $dir = dirname($path);
        if (!$wp_filesystem->is_dir($dir)) {
            $return = $wp_filesystem->mkdir($dir, 0755);
            if (!$return) {
                $this->result_code = self::RES_FAILURE;
                return false;
            }
        }

        if (is_object($value)) {
            $value = clone $value;
        }

        $return = $wp_filesystem->put_contents($path, @serialize($value));
        if (!$return) {
            $this->result_code = self::RES_FAILURE;
            return false;
        }

        $this->result_code = self::RES_SUCCESS;
        return true;
    }

---

// object-cache-disk-template.php line 211
    private function _get_path($key)
    {
        $hash = md5($key); //Returns the hash as a 32-character hexadecimal number.

        $array_hash = str_split($hash, 8); //8 name based

        $path = $this->local_path . implode(DIRECTORY_SEPARATOR, $array_hash);
        
        // Ensure directory exists with an index.php to prevent directory listing
        if (!file_exists($path . DIRECTORY_SEPARATOR . 'index.php')) {
            @file_put_contents($path . DIRECTORY_SEPARATOR . 'index.php', '<?php // Silence is golden.');
        }

        $path .= DIRECTORY_SEPARATOR . '.object.php';
        return $path;
    }

Security Fix

Only in /home/deploy/wp-safety.org/data/plugin-versions/object-cache-4-everyone/2.3.3: changelog.txt
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/object-cache-4-everyone/2.3.2/object-cache-4-everyone.php /home/deploy/wp-safety.org/data/plugin-versions/object-cache-4-everyone/2.3.3/object-cache-4-everyone.php
--- /home/deploy/wp-safety.org/data/plugin-versions/object-cache-4-everyone/2.3.2/object-cache-4-everyone.php	2026-05-08 11:59:40.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/object-cache-4-everyone/2.3.3/object-cache-4-everyone.php	2026-06-11 11:41:48.000000000 +0000
@@ -5,7 +5,7 @@
  * Description: Memcached or disk backend support for the WP Object Cache. Memcached server running and PHP Memcached class needed for better performance. No configuration needed, runs automatically
  * Plugin URI: https://wordpress.org/plugins/object-cache-4-everyone
  * Author: fpuenteonline
- * Version: 2.3.2
+ * Version: 2.3.3
  * Author URI: https://twitter.com/fpuenteonline
  * License:     GPLv2 or later
  * License URI: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
@@ -133,7 +133,7 @@
                 '127.0.0.1:20001'
             );
 
-            if (defined('OC4EVERYONE_MEMCACHED_SERVER')) {
+            if (defined('OC4EVERYONE_MEMCACHED_SERVER') && is_string(OC4EVERYONE_MEMCACHED_SERVER) && preg_match('/^[A-Za-z0-9.\\-]+:[0-9]{1,5}$/', OC4EVERYONE_MEMCACHED_SERVER)) {
                 $memcached_servers =  array(OC4EVERYONE_MEMCACHED_SERVER);
             } else {
                 // Try SG Memcached server first.
@@ -188,8 +188,8 @@
  *
  */
 ?>" . $template;
-                $template .= '//Detected memcached server - ' . gmdate('d/m/Y G:i:s', current_time('timestamp', 0)) . PHP_EOL;
-                $template .= "define('OC4EVERYONE_PREDEFINED_SERVER', '$found_server');" . PHP_EOL;
+                $template .= '//Detected memcached server - ' . gmdate('d/m/Y G:i:s', time()) . PHP_EOL;
+                $template .= 'define(\'OC4EVERYONE_PREDEFINED_SERVER\', ' . var_export($found_server, true) . ');' . PHP_EOL;
 
                 $template .= "if (! defined('WP_CACHE_KEY_SALT')) {" . PHP_EOL;
                 global $wpdb;
@@ -231,8 +231,8 @@
  */
 ?>" . $template;
 
-        $template .= '//No detected memcached server - ' . gmdate('d/m/Y G:i:s', current_time('timestamp', 0)) . PHP_EOL;
-        $template .= "define('OC4EVERYONE_PREDEFINED_SERVER', '');" . PHP_EOL;
+        $template .= '//No detected memcached server - ' . gmdate('d/m/Y G:i:s', time()) . PHP_EOL;
+        $template .= 'define(\'OC4EVERYONE_PREDEFINED_SERVER\', ' . var_export('', true) . ');' . PHP_EOL;
 
         $template .= "if (! defined('WP_CACHE_KEY_SALT')) {" . PHP_EOL;
         $template .= "define('WP_CACHE_KEY_SALT', '" . filemtime(__FILE__) . "');" . PHP_EOL;
@@ -254,31 +254,40 @@
     global $wp_object_cache;
 
     if (strpos($plugin_file_name, basename(__FILE__)) && class_exists('Memcached') && method_exists($wp_object_cache, 'getStats') && file_exists(WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'object-cache.php')) {
+        // Read the stats once; every value below comes from the Memcached server, so it is treated as untrusted and escaped on output.
+        $all_stats = $wp_object_cache->getStats();
+
         // Extra check.
-        if(!defined('OC4EVERYONE_PREDEFINED_SERVER') || !array_key_exists(OC4EVERYONE_PREDEFINED_SERVER, $wp_object_cache->getStats())){
-            return $links_array;        
+        if (!defined('OC4EVERYONE_PREDEFINED_SERVER') || !array_key_exists(OC4EVERYONE_PREDEFINED_SERVER, $all_stats)) {
+            return $links_array;
         }
 
-        $hits = $wp_object_cache->getStats()[OC4EVERYONE_PREDEFINED_SERVER]['get_hits'];
+        $stats = $all_stats[OC4EVERYONE_PREDEFINED_SERVER];
+
+        $hits = isset($stats['get_hits']) ? (int) $stats['get_hits'] : 0;
         // Extra check.
-        if($hits == 0) {
-            return $links_array;        
+        if ($hits === 0) {
+            return $links_array;
         }
-        $misses = $wp_object_cache->getStats()[OC4EVERYONE_PREDEFINED_SERVER]['get_misses'];
-        $total = $hits + $misses;
-        $found = @round((100 / $total) * $hits, 2);
+        $misses = isset($stats['get_misses']) ? (int) $stats['get_misses'] : 0;
+        $total  = $hits + $misses;
+        $found  = $total > 0 ? round((100 / $total) * $hits, 2) : 0;
 
         if (defined('WP_DEBUG') && WP_DEBUG) {
-            error_log('Object Cache 4 everyone::Memcached Server running');
+            error_log('Object Cache 4 everyone::Memcached Server running'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
         }
         $nonce = wp_create_nonce('flush_memcached_nonce');
 
-        $links_array[] = '<a href="' . esc_url(admin_url('admin-post.php?action=oc4flush_memcached&nonce=' . $nonce)) . '"><strong>' . esc_html__('Flush cache', 'object-cache-4-everyone') . '</strong></a>' . 
+        $uptime      = isset($stats['uptime']) ? (int) $stats['uptime'] : 0;
+        $curr_items  = isset($stats['curr_items']) ? $stats['curr_items'] : 0;
+        $total_items = isset($stats['total_items']) ? $stats['total_items'] : 0;
+
+        $links_array[] = '<a href="' . esc_url(admin_url('admin-post.php?action=oc4flush_memcached&nonce=' . $nonce)) . '"><strong>' . esc_html__('Flush cache', 'object-cache-4-everyone') . '</strong></a>' .
             '<br/><br/>' .
-            esc_html__('Memcached Server running:', 'object-cache-4-everyone') . ' <strong><code style="background-color: inherit;">' . OC4EVERYONE_PREDEFINED_SERVER . '</code></strong>' . '<br/>' .
-            esc_html__('Cache Hit Ratio', 'object-cache-4-everyone') . ' <strong><code style="background-color: inherit;">' . $found . '%</code></strong>' . '<br/>' .
-            esc_html__('Uptime:', 'object-cache-4-everyone')  . ' <strong><code style="background-color: inherit;">' . secondsToHumanReadable($wp_object_cache->getStats()[OC4EVERYONE_PREDEFINED_SERVER]['uptime']) . '</strong></code>' . '<br/>' .
-            esc_html__('Current Unique Items / Total Items:', 'object-cache-4-everyone') . ' <strong><code style="background-color: inherit;">' . $wp_object_cache->getStats()[OC4EVERYONE_PREDEFINED_SERVER]['curr_items'] . ' / ' . $wp_object_cache->getStats()[OC4EVERYONE_PREDEFINED_SERVER]['total_items'] . '</strong></code>' . '<br/>';
+            esc_html__('Memcached Server running:', 'object-cache-4-everyone') . ' <strong><code style="background-color: inherit;">' . esc_html(OC4EVERYONE_PREDEFINED_SERVER) . '</code></strong>' . '<br/>' .
+            esc_html__('Cache Hit Ratio', 'object-cache-4-everyone') . ' <strong><code style="background-color: inherit;">' . esc_html($found) . '%</code></strong>' . '<br/>' .
+            esc_html__('Uptime:', 'object-cache-4-everyone')  . ' <strong><code style="background-color: inherit;">' . esc_html(secondsToHumanReadable($uptime)) . '</code></strong>' . '<br/>' .
+            esc_html__('Current Unique Items / Total Items:', 'object-cache-4-everyone') . ' <strong><code style="background-color: inherit;">' . absint($curr_items) . ' / ' . absint($total_items) . '</code></strong>' . '<br/>';
     }
 
     return $links_array;
@@ -288,6 +297,10 @@
 
 add_action('admin_post_oc4flush_memcached', 'oc4flush_memcached');
 function oc4flush_memcached() {
+    // Authorization: only administrators may flush the cache. The nonce alone is not an authorization check.
+    if (!current_user_can('manage_options')) {
+        wp_die(esc_html__('Access denied.', 'object-cache-4-everyone'), '', array('response' => 403));
+    }
     if (!class_exists('Memcached')) {
         wp_die(esc_html__('Failed to flush Memcached server', 'object-cache-4-everyone'));
     }
@@ -295,7 +308,6 @@
     if (isset($_GET['nonce']) && wp_verify_nonce(sanitize_key($_GET['nonce']), 'flush_memcached_nonce')) {
         // Flush cache.
         global $wp_object_cache;
-        error_log(print_r($wp_object_cache->getStats()[OC4EVERYONE_PREDEFINED_SERVER], true));        
         $wp_object_cache->flush();
 
         $memcached = new Memcached();
@@ -304,7 +316,7 @@
 
         if ($memcached->flush(0)) {
             if (defined('WP_DEBUG') && WP_DEBUG) {
-                error_log('Object Cache 4 everyone::Memcached server flushed.');
+                error_log('Object Cache 4 everyone::Memcached server flushed.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
             }
             wp_redirect(admin_url('plugins.php'));
             exit;
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/object-cache-4-everyone/2.3.2/object-cache-disk-template.php /home/deploy/wp-safety.org/data/plugin-versions/object-cache-4-everyone/2.3.3/object-cache-disk-template.php
--- /home/deploy/wp-safety.org/data/plugin-versions/object-cache-4-everyone/2.3.2/object-cache-disk-template.php	2026-05-08 10:42:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/object-cache-4-everyone/2.3.3/object-cache-disk-template.php	2026-06-11 11:41:48.000000000 +0000
@@ -33,9 +33,49 @@
                 return;
             }
         }
+        // Block direct web access and directory listing of the cache tree.
+        $this->protect_dir($this->local_path);
+
         $this->result_code = self::RES_SUCCESS;
     }
 
+    /**
+     * Drops an index.php and a deny-all .htaccess in a cache directory so the
+     * serialized cache files cannot be listed or served over the web.
+     * @param string $dir Directory to protect (with trailing separator).
+     */
+    private function protect_dir($dir)
+    {
+        global $wp_filesystem;
+
+        if (!$wp_filesystem->exists($dir . 'index.php')) {
+            $wp_filesystem->put_contents($dir . 'index.php', '<?php // Silence is golden.');
+        }
+
+        if (!$wp_filesystem->exists($dir . '.htaccess')) {
+            $htaccess = "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nOrder allow,deny\nDeny from all\n</IfModule>\n";
+            $wp_filesystem->put_contents($dir . '.htaccess', $htaccess);
+        }
+    }
+
+    /**
+     * Returns the secret used to sign cache files on disk.
+     * @return string
+     */
+    private function hmac_key()
+    {
+        if (defined('AUTH_SALT') && AUTH_SALT) {
+            return AUTH_SALT;
+        }
+        if (defined('SECURE_AUTH_SALT') && SECURE_AUTH_SALT) {
+            return SECURE_AUTH_SALT;
+        }
+        return 'oc4everyone-disk-cache';
+    }
+
     public function quit()
     {
         return false;
@@ -125,7 +165,11 @@
             $value = clone $value;
         }
 
-        $return = $wp_filesystem->put_contents($path, @serialize($value));
+        // Sign the serialized payload so a tampered cache file is rejected before unserialize().
+        $serialized = @serialize($value);
+        $blob       = hash_hmac('sha256', $serialized, $this->hmac_key()) . $serialized;
+
+        $return = $wp_filesystem->put_contents($path, $blob);
         if (!$return) {
             $this->result_code = self::RES_FAILURE;
             return false;
@@ -146,12 +190,22 @@
         }
 
         $objData = $wp_filesystem->get_contents($path);
-        if ($objData === false) {
+        if ($objData === false || strlen($objData) < 64) {
             $this->result_code = self::RES_FAILURE;
             return false;
         }
 
-        $data = unserialize($objData);
+        // Verify the HMAC signature before unserializing.
+        $stored_hmac = substr($objData, 0, 64);
+        $serialized  = substr($objData, 64);
+        if (!hash_equals(hash_hmac('sha256', $serialized, $this->hmac_key()), $stored_hmac)) {
+            $this->result_code = self::RES_FAILURE;
+            return false;
+        }
+
+        $data = unserialize($serialized);
 
         $this->result_code = self::RES_SUCCESS;
         return $data;
@@ -164,13 +218,9 @@
         $array_hash = str_split($hash, 8); //8 name based
 
         $path = $this->local_path . implode(DIRECTORY_SEPARATOR, $array_hash);
-        
-        // Ensure directory exists with an index.php to prevent directory listing
-        if (!file_exists($path . DIRECTORY_SEPARATOR . 'index.php')) {
-            @file_put_contents($path . DIRECTORY_SEPARATOR . 'index.php', '<?php // Silence is golden.');
-        }
 
-        $path .= DIRECTORY_SEPARATOR . '.object.php';
+        // Cache payload stored with a non-PHP extension so it is never executed even if served directly.
+        $path .= DIRECTORY_SEPARATOR . '.object.cache';
         return $path;
     }
 }

Exploit Outline

The exploit involves predicting the URI of sensitive cache entries stored on disk when Memcached is unavailable. The plugin generates paths by calculating the MD5 hash of a cache key (such as '1:options:alloptions' for site settings or '1:users:1' for user metadata) and splitting that hash into 8-character segments to form a directory structure within '/wp-content/cache/object/'. Since these files use the '.object.php' extension and are not protected by .htaccess or authorization checks in vulnerable versions, an unauthenticated attacker can perform a direct HTTP GET request to the calculated path to download the serialized PHP data and extract sensitive information.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.