WordPress PHP Object Injection Vulnerability

Critical Risk Object Injection
wordpressphpobject-injectiondeserializationpluginsremote-code-execution

What it is

The WordPress plugin contains PHP object injection vulnerabilities through unsafe deserialization of user-controlled data. This occurs when plugins use unserialize() on untrusted input, potentially allowing attackers to execute arbitrary code, manipulate application logic, or perform other malicious actions through crafted serialized objects.

// Vulnerable: WordPress plugin with object injection // In a WordPress plugin file class VulnerablePlugin { public function __construct() { add_action('wp_ajax_save_settings', [$this, 'save_settings']); add_action('wp_ajax_nopriv_save_settings', [$this, 'save_settings']); } public function save_settings() { // Dangerous: Direct unserialize of user input $settings = unserialize($_POST['settings']); update_option('plugin_settings', $settings); wp_die('Settings saved'); } // Another vulnerable pattern public function import_data() { if (isset($_POST['import_data'])) { // Dangerous: Unserializing uploaded file content $data = unserialize(file_get_contents($_FILES['import']['tmp_name'])); foreach ($data as $item) { // Process imported data $this->process_item($item); } } } } // Dangerous class with magic methods class FileManager { private $filename; public function __construct($filename) { $this->filename = $filename; } // Dangerous: Destructor can be triggered during deserialization public function __destruct() { if (file_exists($this->filename)) { unlink($this->filename); // Could delete arbitrary files } } public function __wakeup() { // Dangerous: Code execution during deserialization eval($this->filename); // Extremely dangerous } }
// Secure: WordPress plugin with proper input handling class SecurePlugin { public function __construct() { add_action('wp_ajax_save_settings', [$this, 'save_settings']); // Note: Removed nopriv action for security } public function save_settings() { // Security: Verify nonce if (!wp_verify_nonce($_POST['nonce'], 'save_settings_nonce')) { wp_die('Security check failed'); } // Security: Check user capabilities if (!current_user_can('manage_options')) { wp_die('Insufficient permissions'); } // Secure: Use JSON instead of serialize if (isset($_POST['settings'])) { $settings_json = sanitize_textarea_field($_POST['settings']); // Validate JSON format $settings = json_decode($settings_json, true); if (json_last_error() !== JSON_ERROR_NONE) { wp_die('Invalid settings format'); } // Validate settings structure $validated_settings = $this->validate_settings($settings); update_option('plugin_settings', $validated_settings); wp_die('Settings saved successfully'); } } private function validate_settings($settings) { $allowed_keys = ['theme', 'language', 'notifications']; $validated = []; foreach ($allowed_keys as $key) { if (isset($settings[$key])) { switch ($key) { case 'theme': $validated[$key] = in_array($settings[$key], ['light', 'dark']) ? $settings[$key] : 'light'; break; case 'language': $validated[$key] = sanitize_text_field($settings[$key]); break; case 'notifications': $validated[$key] = (bool) $settings[$key]; break; } } } return $validated; } // Secure import functionality public function import_data() { // Security checks if (!wp_verify_nonce($_POST['import_nonce'], 'import_data_nonce')) { wp_die('Security check failed'); } if (!current_user_can('import')) { wp_die('Insufficient permissions'); } if (isset($_FILES['import']) && $_FILES['import']['error'] === UPLOAD_ERR_OK) { // Validate file type $allowed_types = ['application/json', 'text/plain']; $file_type = $_FILES['import']['type']; if (!in_array($file_type, $allowed_types)) { wp_die('Invalid file type. Only JSON files allowed.'); } // Read and validate file content $file_content = file_get_contents($_FILES['import']['tmp_name']); // Secure: Use JSON instead of unserialize $data = json_decode($file_content, true); if (json_last_error() !== JSON_ERROR_NONE) { wp_die('Invalid JSON format'); } // Validate data structure if (!is_array($data)) { wp_die('Invalid data format'); } foreach ($data as $item) { $this->process_item_safely($item); } } } private function process_item_safely($item) { // Validate each item before processing if (!is_array($item) || !isset($item['id'], $item['title'])) { return false; // Skip invalid items } $safe_item = [ 'id' => intval($item['id']), 'title' => sanitize_text_field($item['title']), 'content' => isset($item['content']) ? wp_kses_post($item['content']) : '', 'status' => isset($item['status']) && in_array($item['status'], ['draft', 'published']) ? $item['status'] : 'draft' ]; // Process the validated item return wp_insert_post($safe_item); } } // Secure class design without dangerous magic methods class SecureFileManager { private $allowed_files = []; private $base_path; public function __construct($base_path) { $this->base_path = realpath($base_path); if (!$this->base_path) { throw new InvalidArgumentException('Invalid base path'); } } public function add_allowed_file($filename) { $safe_filename = sanitize_file_name($filename); $full_path = realpath($this->base_path . '/' . $safe_filename); // Ensure file is within base path if ($full_path && strpos($full_path, $this->base_path) === 0) { $this->allowed_files[] = $full_path; } } public function delete_file($filename) { $safe_filename = sanitize_file_name($filename); $full_path = realpath($this->base_path . '/' . $safe_filename); // Security: Only delete allowed files within base path if ($full_path && strpos($full_path, $this->base_path) === 0 && in_array($full_path, $this->allowed_files)) { return unlink($full_path); } return false; } // No dangerous magic methods }

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

WordPress plugin developers call PHP's unserialize() function directly on data provided by users through HTTP requests, AJAX handlers, or shortcode parameters, creating object injection vulnerabilities that enable remote code execution. Plugin code that processes wp_ajax actions frequently deserializes $_POST or $_GET data without validation: unserialize($_POST['data']) allows attackers to craft malicious serialized payloads containing arbitrary object instances. WordPress's extensive class ecosystem provides numerous gadget chains—sequences of magic methods (__wakeup, __destruct, __toString) that execute when objects are deserialized—enabling attackers to chain method calls for arbitrary code execution or file manipulation. Example attack: an attacker submits a serialized object that, when deserialized, triggers a __destruct method deleting critical files or a __wakeup method executing system commands. WordPress's autoloading of plugin and theme classes expands the available gadget chains significantly, making exploitation easier as attackers can leverage classes from any active plugin. The wp_ajax_ and wp_ajax_nopriv_ hooks commonly used for AJAX endpoints frequently lack proper security validation, accepting and deserializing user input without checking capabilities or nonces. Plugin developers accustomed to WordPress's serialize/unserialize patterns for options storage mistakenly apply the same approach to untrusted user input without recognizing the security implications.

Root causes

Using unserialize() on User-Controlled Input in WordPress Plugins

WordPress plugin developers call PHP's unserialize() function directly on data provided by users through HTTP requests, AJAX handlers, or shortcode parameters, creating object injection vulnerabilities that enable remote code execution. Plugin code that processes wp_ajax actions frequently deserializes $_POST or $_GET data without validation: unserialize($_POST['data']) allows attackers to craft malicious serialized payloads containing arbitrary object instances. WordPress's extensive class ecosystem provides numerous gadget chains—sequences of magic methods (__wakeup, __destruct, __toString) that execute when objects are deserialized—enabling attackers to chain method calls for arbitrary code execution or file manipulation. Example attack: an attacker submits a serialized object that, when deserialized, triggers a __destruct method deleting critical files or a __wakeup method executing system commands. WordPress's autoloading of plugin and theme classes expands the available gadget chains significantly, making exploitation easier as attackers can leverage classes from any active plugin. The wp_ajax_ and wp_ajax_nopriv_ hooks commonly used for AJAX endpoints frequently lack proper security validation, accepting and deserializing user input without checking capabilities or nonces. Plugin developers accustomed to WordPress's serialize/unserialize patterns for options storage mistakenly apply the same approach to untrusted user input without recognizing the security implications.

Processing Serialized Data from POST/GET Parameters Without Validation

WordPress plugins accept serialized PHP data through HTTP request parameters and deserialize it without format validation, type checking, or signature verification, allowing attackers to inject malicious serialized objects directly into deserialization contexts. $_REQUEST, $_POST, and $_GET superglobals are commonly used to retrieve serialized data that is immediately passed to unserialize(): $config = unserialize($_GET['config']) creates direct object injection vectors accessible to any attacker with knowledge of the endpoint. URL parameters in WordPress admin pages, REST API endpoints, or public-facing AJAX handlers may accept base64-encoded or URL-encoded serialized data that plugins decode and deserialize without security checks. Shortcode implementations that accept serialized attributes enable object injection through post content: [custom_shortcode data="<serialized_payload>"] allows content editors or attackers with contributor access to inject objects. Form submission handlers that expect serialized arrays for complex data structures fail to validate the serialized format before deserialization, trusting client-side serialization without server-side verification. Hidden form fields containing serialized session data or state information can be modified by attackers using browser developer tools or proxy interception. WordPress's update_option() and get_option() functions use serialize/unserialize internally for complex data types, and plugins that store user-provided serialized data in options inadvertently create persistent object injection vectors exploitable on option retrieval.

Deserializing Data from Cookies, Database, or External Sources

WordPress plugins deserialize data retrieved from cookies, database records, transients, or external API responses without verifying data integrity or origin, allowing attackers to inject malicious objects through these storage and transmission channels. Cookie-based object injection occurs when plugins store serialized user preferences or session data in cookies: setcookie('user_data', serialize($data)) followed by unserialize($_COOKIE['user_data']) allows attackers to modify cookies and inject objects. Since cookies are entirely controlled by clients, any serialized data in cookies represents untrusted input that should never be deserialized without cryptographic verification. WordPress transients stored in the database may contain serialized data from external sources like API responses, and plugins that deserialize transient values without validation create injection points: $data = unserialize(get_transient('api_cache')) is vulnerable if transients can be poisoned through SQL injection or direct database access. Database columns storing serialized plugin settings, user metadata, or cached results become attack vectors when plugins deserialize this data without confirming its origin or integrity, particularly in multi-author WordPress installations where lower-privileged users might manipulate their own metadata. Import/export functionality that deserializes uploaded files or pasted text creates object injection risks: file_get_contents($_FILES['import']['tmp_name']) followed by unserialize() allows attackers to upload malicious serialized payloads disguised as legitimate backup files. Integration with external services where plugins deserialize API responses without validation extends trust to third-party systems that could be compromised or malicious.

Missing Input Validation Before Deserialization Operations

WordPress plugins fail to implement validation checks before deserializing data, omitting essential security controls like format verification, signature validation, allowlist checking, or capability requirements that could prevent object injection attacks. Plugins deserialize data without first verifying it matches expected serialized format patterns: checking that strings begin with expected serialized prefixes like 'a:' (array), 's:' (string), or 'O:' (object) and contain valid length specifications can detect anomalous payloads, yet plugins rarely perform such validation. Cryptographic signatures using HMAC algorithms could verify serialized data hasn't been tampered with, but WordPress plugins typically lack signature generation during serialization and validation before deserialization: hash_hmac('sha256', $serialized_data, SECRET_KEY) signatures would prevent cookie and transient tampering. WordPress capability checks using current_user_can() are frequently missing from deserialization contexts, allowing unauthenticated users or low-privilege accounts to trigger object injection vulnerabilities through AJAX handlers registered with both wp_ajax_ and wp_ajax_nopriv_ hooks. Plugins fail to validate data origin, deserializing data from any source without distinguishing between trusted internal storage and untrusted user input. Content-Security-Policy headers and nonce verification through wp_verify_nonce() are absent from AJAX endpoints that deserialize user data, leaving them vulnerable to cross-site request forgery combined with object injection. Input sanitization using WordPress functions like sanitize_text_field() or wp_kses() occurs after deserialization rather than before, missing the opportunity to prevent malicious objects from being instantiated in the first place.

Presence of Dangerous Magic Methods in Plugin Classes

WordPress plugins and themes define classes with dangerous magic methods (__destruct, __wakeup, __toString, __call) that perform security-sensitive operations, creating exploitable gadget chains that attackers trigger through object injection to achieve code execution or data manipulation. __destruct methods that delete files, execute system commands, or write to disk without validating object state become weaponized when attackers inject instances of these classes: a FileCache class with __destruct() { unlink($this->filename); } allows arbitrary file deletion when attackers control the filename property through injected objects. __wakeup methods executing immediately upon deserialization frequently perform initialization tasks like database queries, file operations, or external API calls using object properties that attackers fully control through serialized data. __toString methods invoked when objects are used in string contexts (echo, concatenation, string comparison) may execute queries, render templates, or log messages that attackers exploit by triggioning __toString through specific code paths. Property-oriented programming (POP) chains combine multiple classes where one class's magic method calls another class's method, eventually reaching a dangerous operation: ClassA::__destruct() calls $this->logger->log() which is actually ClassB::__call() containing eval() enables complex multi-step exploits. WordPress core classes and popular plugin frameworks like WooCommerce, Advanced Custom Fields, or Elementor provide extensive class libraries with various magic methods, expanding the available gadget chains for attackers to exploit. Plugin developers unaware of object injection risks implement magic methods for legitimate functionality without considering how attackers might abuse these methods through deserialization, lacking defensive programming practices like validating object state before performing sensitive operations in magic methods.

Fixes

1

Replace unserialize() with Safe Alternatives Like JSON for All User Input

Migrate WordPress plugin data handling from PHP's serialize/unserialize to JSON encoding for all user-controlled data, eliminating object injection attack vectors while maintaining data structure capabilities. Replace serialize($data) and unserialize($input) patterns with json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) and json_decode($input, true) which only produce scalar values and arrays, never object instances that could contain malicious methods. JSON deserialization is inherently safe from object injection because JSON specification does not support object types, class information, or code execution—it represents only primitives, arrays, and nested structures. For AJAX handlers, send JSON from client side using JSON.stringify() and parse with json_decode() on server: $settings = json_decode(file_get_contents('php://input'), true) safely handles POST bodies without object injection risks. Validate JSON parsing success with json_last_error() checks: if (json_last_error() !== JSON_ERROR_NONE) { wp_die('Invalid data format'); } prevents processing of malformed input. For WordPress options storage, continue using update_option() which handles serialization internally and safely, but never deserialize untrusted external data. For complex objects requiring serialization, use __sleep() and __wakeup() magic methods defensively with strict property validation, or better yet, implement explicit toArray() and fromArray() methods that convert objects to safe array representations for JSON encoding. When migrating legacy code, maintain backward compatibility by detecting data format: $data = (substr($input, 0, 2) === 'a:') ? unserialize($input) : json_decode($input, true) allows gradual migration while preferring JSON for new data.

2

Validate and Sanitize All Input Before Any Deserialization Operation

Implement comprehensive input validation and sanitization before deserializing any data in WordPress plugins, applying multiple security controls that verify data format, integrity, origin, and expected structure before instantiating objects. For any remaining use of unserialize(), validate that input matches expected serialized format patterns using regular expressions: preg_match('/^(a|O|s|i|d|b|N):[0-9]+:/', $input) ensures data begins with valid serialization prefixes. Implement cryptographic signatures for serialized data stored in cookies or client-controlled locations: sign data during serialization using hash_hmac('sha256', $serialized, wp_salt('auth')), store signature separately, and verify before deserialization. Check data origin by restricting deserialization to data from trusted sources: never deserialize direct user input from $_POST, $_GET, $_COOKIE, or uploaded files. Validate user capabilities before deserializing: if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } limits exposure to privileged users. Implement nonce verification using wp_verify_nonce() for all AJAX endpoints that deserialize data: check_ajax_referer('action_name', 'nonce') prevents CSRF attacks combined with object injection. Sanitize input using WordPress sanitization functions before deserialization: $safe_input = wp_unslash($_POST['data']); sanitize_text_field($safe_input) removes dangerous characters. Validate expected data structure after deserialization: if (!is_array($data) || !isset($data['required_key'])) { wp_die('Invalid structure'); } ensures deserialized data matches expectations. Use allowlist validation where only known-good values are accepted: in_array($data['type'], ['option1', 'option2', 'option3']) prevents injection of unexpected values or types.

3

Use WordPress's Built-in Sanitization and Security Functions

Leverage WordPress's extensive security API including sanitization functions, capability checks, nonce verification, and safe data access methods to build defense-in-depth protection against object injection and related vulnerabilities. Apply sanitization functions appropriate to data type and context: sanitize_text_field() for single-line text, sanitize_textarea_field() for multi-line text, sanitize_email() for email addresses, sanitize_url() for URLs, and wp_kses_post() for HTML content. Use esc_* functions for output escaping: esc_html(), esc_attr(), esc_url(), esc_js() prevent cross-site scripting when displaying deserialized data. Implement capability checks using current_user_can('capability') before deserializing or processing sensitive data: capabilities like 'manage_options', 'edit_posts', 'import' restrict functionality to appropriate user roles. Use check_admin_referer('action_name') for form submissions and check_ajax_referer('action_name', 'nonce') for AJAX requests to prevent CSRF attacks that could trigger deserialization with attacker-controlled data. Register AJAX handlers correctly: use add_action('wp_ajax_my_action', 'callback') for authenticated users only, avoiding wp_ajax_nopriv_ hooks for sensitive operations to prevent unauthenticated exploitation. Validate file uploads using wp_check_filetype_and_ext() and WordPress's upload security: if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) handle upload errors properly. Use WordPress's database preparation methods: $wpdb->prepare('SELECT * FROM table WHERE id = %d', $id) prevents SQL injection when storing or retrieving serialized data. Implement sanitize_key() for array keys and sanitize_file_name() for file paths to prevent path traversal combined with object injection. Use wp_safe_redirect() instead of raw header() redirects to prevent open redirect vulnerabilities that could aid in exploitation chains.

4

Implement allowed_classes Option When unserialize() is Absolutely Necessary

When JSON alternatives are impractical and unserialize() must be used for internal data structures, implement the allowed_classes option introduced in PHP 7.0 to restrict which classes can be instantiated during deserialization, dramatically reducing object injection attack surface. Use unserialize($data, ['allowed_classes' => false]) to deserialize data into __PHP_Incomplete_Class objects instead of actual class instances, preventing execution of any magic methods: this approach works when only array and scalar data is needed from serialized structures. Specify an explicit allowlist of safe classes: unserialize($data, ['allowed_classes' => ['stdClass', 'SafeDataClass']]) permits only whitelisted classes to be instantiated, blocking attackers from instantiating gadget chain classes. Review allowed classes to ensure they contain no dangerous magic methods: classes with __destruct, __wakeup, __toString, or __call methods performing security-sensitive operations should not appear in allowed_classes arrays. For internal serialization between trusted application components, serialize only plain arrays or stdClass instances: $safe_data = json_decode(json_encode($object), false) converts objects to stdClass, removing class-specific methods while preserving structure. Document why unserialize() is necessary and what alternatives were considered: code comments explaining serialization decisions help future maintainers understand security implications. Implement serialization verification: before serializing data for storage, validate that it contains only allowed classes using instanceof checks. Consider implementing custom serialization methods: create safe serialize_safe() and unserialize_safe() wrappers that enforce allowed_classes, validation, and signature verification consistently throughout the plugin. Monitor PHP version compatibility: allowed_classes requires PHP 7.0+, so check version or implement polyfills for older environments.

5

Remove or Secure Dangerous Magic Methods in Plugin Classes

Audit WordPress plugin class definitions to identify and remediate dangerous magic methods that create object injection gadget chains, either removing unnecessary methods or implementing defensive validation within them. Review all classes for __destruct methods and ensure they validate object state before performing destructive operations: instead of __destruct() { unlink($this->file); }, implement __destruct() { if (is_string($this->file) && $this->validated && strpos(realpath($this->file), $this->safe_path) === 0) { unlink($this->file); } } with property validation and path restrictions. Examine __wakeup methods that execute during deserialization and add validation: check that object properties contain expected types and values before performing operations, throw exceptions for invalid state: if (!$this->validate()) { throw new Exception('Invalid object state'); }. Remove __wakeup and __destruct methods when not strictly necessary: many classes include these methods for convenience without security implications considered, and removing them eliminates gadget chain possibilities. Avoid __toString methods that perform security-sensitive operations like database queries, file operations, or template rendering: __toString should only return simple string representations of object state. Be cautious with __call and __callStatic methods that dynamically invoke other methods: these create complex gadget chains where attackers control method names and arguments through object properties. Implement __sleep to explicitly control which properties are serialized: public function __sleep() { return ['safe_property']; } prevents serialization of sensitive properties like database connections or file handles. Use final classes when possible to prevent inheritance-based gadget chains: final class SecureClass prevents attackers from creating subclasses with malicious magic methods. Perform static analysis using tools like PHPCS with security rulesets, or specialized tools like Psalm or PHPStan to detect dangerous patterns in magic methods.

6

Use WordPress Nonces for CSRF Protection on All Deserialization Endpoints

Implement WordPress nonces (numbers used once) to prevent cross-site request forgery attacks that could trick authenticated users into triggering object injection vulnerabilities through attacker-controlled deserialization payloads. Generate nonces when rendering forms or AJAX endpoints: wp_nonce_field('save_settings_action', 'save_settings_nonce') creates a hidden form field with cryptographic token unique to user session. Verify nonces before processing requests containing serialized data: check_admin_referer('save_settings_action', 'save_settings_nonce') validates nonce and dies with error message if verification fails. For AJAX requests, use check_ajax_referer('ajax_action_name', 'nonce_parameter') which returns JSON error for failed verification appropriate for JavaScript handling. Pass nonces to JavaScript using wp_localize_script(): wp_localize_script('my-script', 'myAjax', ['nonce' => wp_create_nonce('my_ajax_action')]) makes nonces available for AJAX requests. Include nonces in AJAX requests: $.post(ajaxurl, { action: 'my_action', nonce: myAjax.nonce, data: JSON.stringify(data) }) sends nonce for server-side verification. Use specific action names for nonces rather than generic names: each distinct operation should have its own nonce action to prevent nonce reuse across different security contexts. Verify nonces before deserializing any data from requests: the sequence should be (1) verify nonce, (2) check capabilities, (3) sanitize input, (4) deserialize, (5) validate structure. Configure nonce lifetime appropriately using nonce_life filter if default 24-hour lifetime is inappropriate for your use case. Implement nonce refresh for long-running single-page applications: refresh nonces via AJAX to prevent expiration during active user sessions. Combine nonces with capability checks for defense-in-depth: check_ajax_referer('action'); if (!current_user_can('manage_options')) { wp_die('Unauthorized'); } ensures both CSRF protection and authorization. Document nonce requirements in plugin development guidelines and enforce nonce usage through code review processes.

Detect This Vulnerability in Your Code

Sourcery automatically identifies wordpress php object injection vulnerability and many other security issues in your codebase.