From 4680f5c69666368ec68e8fd7dfdc992ff7ce9768 Mon Sep 17 00:00:00 2001 From: Sertii <36940685+Sreini@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:34:38 +0100 Subject: [PATCH 1/7] release: 3.6.10 --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 565e7c67..aa6e9ea1 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Donate link: https://tinypng.com/ Tags: compress images, compression, image size, page speed, performance Requires at least: 4.0 Tested up to: 6.9 -Stable tag: 3.6.9 +Stable tag: 3.6.10 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html From d72f1a647501a28b255759830c9f12708107184c Mon Sep 17 00:00:00 2001 From: tijmen Date: Thu, 7 May 2026 10:17:47 +0200 Subject: [PATCH 2/7] confirm valid path on conversion --- src/class-tiny-image-size.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/class-tiny-image-size.php b/src/class-tiny-image-size.php index ca3fd27e..244751e7 100644 --- a/src/class-tiny-image-size.php +++ b/src/class-tiny-image-size.php @@ -242,7 +242,13 @@ public function duplicate_of_size() { public function delete_converted_image_size() { if ( $this->converted_image_exists() ) { - unlink( $this->meta['convert']['path'] ); + $upload_dir = wp_upload_dir(); + $convert_real_path = realpath( $this->meta['convert']['path'] ); + $real_basedir = realpath( $upload_dir['basedir'] ); + + if ( $convert_real_path && str_starts_with( $convert_real_path, $real_basedir ) ) { + unlink( $convert_real_path ); + } } } From 72631dce9c0eeb0449781aca8fa71499ef415fe6 Mon Sep 17 00:00:00 2001 From: tijmen Date: Thu, 7 May 2026 11:29:36 +0200 Subject: [PATCH 3/7] add tests --- src/class-tiny-image-size.php | 2 +- test/unit/TinyImageSizeTest.php | 94 +++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/class-tiny-image-size.php b/src/class-tiny-image-size.php index 244751e7..f0fe51b7 100644 --- a/src/class-tiny-image-size.php +++ b/src/class-tiny-image-size.php @@ -246,7 +246,7 @@ public function delete_converted_image_size() { $convert_real_path = realpath( $this->meta['convert']['path'] ); $real_basedir = realpath( $upload_dir['basedir'] ); - if ( $convert_real_path && str_starts_with( $convert_real_path, $real_basedir ) ) { + if ( $convert_real_path && str_starts_with( $convert_real_path, trailingslashit( $real_basedir ) ) ) { unlink( $convert_real_path ); } } diff --git a/test/unit/TinyImageSizeTest.php b/test/unit/TinyImageSizeTest.php index 9d61db7d..c77213ad 100644 --- a/test/unit/TinyImageSizeTest.php +++ b/test/unit/TinyImageSizeTest.php @@ -241,6 +241,100 @@ public function test_when_not_compressed_will_mark_as_compressed() $this->assertTrue($image_size->has_been_compressed()); } + /** + * Deletes the converted file when it exists within the upload directory. + * sidenote: we cannot use vfs as realpath prevents virtual file paths. + */ + public function test_delete_converted_image_size_deletes_file_within_upload_dir() { + $tmp_dir = sys_get_temp_dir() . '/tiny-test-' . uniqid(); + $upload_dir = $tmp_dir . '/uploads'; + $file_path = $upload_dir . '/image.webp'; + mkdir( $upload_dir, 0755, true ); + file_put_contents( $file_path, 'webp content' ); + + $this->wp->stub( 'wp_upload_dir', function () use ( $upload_dir ) { + return array( 'basedir' => $upload_dir ); + } ); + + $image_size = new Tiny_Image_Size( $file_path ); + $image_size->meta['convert'] = array( + 'path' => $file_path, + 'type' => 'image/webp', + 'size' => 100, + ); + + $image_size->delete_converted_image_size(); + + $this->assertFalse( file_exists( $file_path ) ); + rmdir( $upload_dir ); + rmdir( $tmp_dir ); + } + + /** + * Does not delete a file that lives outside the upload directory. + */ + public function test_delete_converted_image_size_does_not_delete_file_outside_upload_dir() { + $tmp_dir = sys_get_temp_dir() . '/tiny-test-' . uniqid(); + $upload_dir = $tmp_dir . '/uploads'; + $outside_dir = $tmp_dir . '/outside'; + $file_path = $outside_dir . '/wp-config.php'; + mkdir( $upload_dir, 0755, true ); + mkdir( $outside_dir, 0755, true ); + file_put_contents( $file_path, 'my wp config' ); + + $this->wp->stub( 'wp_upload_dir', function () use ( $upload_dir ) { + return array( 'basedir' => $upload_dir ); + } ); + + $image_size = new Tiny_Image_Size( $file_path ); + $image_size->meta['convert'] = array( + 'path' => $file_path, + 'type' => 'image/webp', + 'size' => 100, + ); + + $image_size->delete_converted_image_size(); + + $this->assertTrue( file_exists( $file_path ), 'file outside upload dir should not be deleted'); + unlink( $file_path ); + rmdir( $outside_dir ); + rmdir( $upload_dir ); + rmdir( $tmp_dir ); + } + + /** + * Does not delete a file in a sibling directory whose name starts with the upload dir name. + * This validates the trailingslashit() protection against sibling-directory bypass. + */ + public function test_delete_converted_image_size_does_not_delete_file_in_sibling_directory() { + $tmp_dir = sys_get_temp_dir() . '/tiny-test-' . uniqid(); + $upload_dir = $tmp_dir . '/uploads'; + $sibling_dir = $tmp_dir . '/uploads-evil'; + $file_path = $sibling_dir . '/shell.webp'; + mkdir( $upload_dir, 0755, true ); + mkdir( $sibling_dir, 0755, true ); + file_put_contents( $file_path, 'malicious content' ); + + $this->wp->stub( 'wp_upload_dir', function () use ( $upload_dir ) { + return array( 'basedir' => $upload_dir ); + } ); + + $image_size = new Tiny_Image_Size( $file_path ); + $image_size->meta['convert'] = array( + 'path' => $file_path, + 'type' => 'image/webp', + 'size' => 100, + ); + + $image_size->delete_converted_image_size(); + + $this->assertTrue( file_exists( $file_path ) ); + unlink( $file_path ); + rmdir( $sibling_dir ); + rmdir( $upload_dir ); + rmdir( $tmp_dir ); + } + /** * Users can still mark an image as converted when compression has already been done. */ From 5f8d3e4d9baa1d5aa6f96812620dc470a146a3c6 Mon Sep 17 00:00:00 2001 From: tijmen Date: Thu, 7 May 2026 11:34:01 +0200 Subject: [PATCH 4/7] add docblock --- src/class-tiny-image-size.php | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/class-tiny-image-size.php b/src/class-tiny-image-size.php index f0fe51b7..5c2a1a53 100644 --- a/src/class-tiny-image-size.php +++ b/src/class-tiny-image-size.php @@ -240,15 +240,23 @@ public function duplicate_of_size() { 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() ) { - $upload_dir = wp_upload_dir(); - $convert_real_path = realpath( $this->meta['convert']['path'] ); - $real_basedir = realpath( $upload_dir['basedir'] ); - - if ( $convert_real_path && str_starts_with( $convert_real_path, trailingslashit( $real_basedir ) ) ) { - unlink( $convert_real_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 && + str_starts_with( $convert_real_path, trailingslashit( $real_basedir ) ) ) { + unlink( $convert_real_path ); } } From 88b171ab9006496feb673b931a98048fa33322f3 Mon Sep 17 00:00:00 2001 From: tijmen Date: Thu, 7 May 2026 11:36:04 +0200 Subject: [PATCH 5/7] Add helper for str_starts_with --- src/class-tiny-helpers.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/class-tiny-helpers.php b/src/class-tiny-helpers.php index 68b4688e..75862960 100644 --- a/src/class-tiny-helpers.php +++ b/src/class-tiny-helpers.php @@ -171,4 +171,28 @@ public static function get_wp_filesystem() { 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 ); + } } From 39b3a0472a1d899769f985b0aab6b3dbc20f700c Mon Sep 17 00:00:00 2001 From: tijmen Date: Thu, 7 May 2026 11:40:46 +0200 Subject: [PATCH 6/7] add tests for helper --- src/class-tiny-image-size.php | 2 +- test/unit/TinyHelpersTest.php | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/class-tiny-image-size.php b/src/class-tiny-image-size.php index 5c2a1a53..2148ab69 100644 --- a/src/class-tiny-image-size.php +++ b/src/class-tiny-image-size.php @@ -255,7 +255,7 @@ public function delete_converted_image_size() { if ( $convert_real_path && - str_starts_with( $convert_real_path, trailingslashit( $real_basedir ) ) ) { + Tiny_Helpers::str_starts_with( $convert_real_path, trailingslashit( $real_basedir ) ) ) { unlink( $convert_real_path ); } } diff --git a/test/unit/TinyHelpersTest.php b/test/unit/TinyHelpersTest.php index 6bf283b5..349a1af8 100644 --- a/test/unit/TinyHelpersTest.php +++ b/test/unit/TinyHelpersTest.php @@ -130,4 +130,44 @@ public function test_is_pagebuilder_request_returns_false_for_non_pagebuilder_ke $this->assertFalse(Tiny_Helpers::is_pagebuilder_request()); $_GET = array(); } + +public function test_str_starts_with_returns_true_when_haystack_starts_with_needle() +{ + $this->assertTrue(Tiny_Helpers::str_starts_with('hello world', 'hello')); +} + +public function test_str_starts_with_returns_false_when_needle_is_not_at_start() +{ + $this->assertFalse(Tiny_Helpers::str_starts_with('hello world', 'world')); +} + +public function test_str_starts_with_returns_true_for_empty_needle() +{ + $this->assertTrue(Tiny_Helpers::str_starts_with('hello', '')); +} + +public function test_str_starts_with_returns_true_when_both_are_empty() +{ + $this->assertTrue(Tiny_Helpers::str_starts_with('', '')); +} + +public function test_str_starts_with_returns_false_when_needle_is_longer_than_haystack() +{ + $this->assertFalse(Tiny_Helpers::str_starts_with('hi', 'hello')); +} + +public function test_str_starts_with_is_case_sensitive() +{ + $this->assertFalse(Tiny_Helpers::str_starts_with('Hello', 'hello')); +} + +public function test_str_starts_with_returns_true_for_path_within_upload_dir() +{ + $this->assertTrue(Tiny_Helpers::str_starts_with('/var/www/uploads/image.webp', '/var/www/uploads/')); +} + +public function test_str_starts_with_returns_false_for_sibling_directory_with_shared_prefix() +{ + $this->assertFalse(Tiny_Helpers::str_starts_with('/var/www/uploads-evil/shell.webp', '/var/www/uploads/')); +} } From 448d71c4f2926146b6057a83463ad750a2caadfd Mon Sep 17 00:00:00 2001 From: tijmen Date: Thu, 7 May 2026 11:50:25 +0200 Subject: [PATCH 7/7] format --- src/class-tiny-helpers.php | 2 +- src/class-tiny-image-size.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/class-tiny-helpers.php b/src/class-tiny-helpers.php index 75862960..f2140d3c 100644 --- a/src/class-tiny-helpers.php +++ b/src/class-tiny-helpers.php @@ -171,7 +171,7 @@ public static function get_wp_filesystem() { return $wp_filesystem; } - + /** * Polyfill for `str_starts_with()` function added in PHP 8.0. * diff --git a/src/class-tiny-image-size.php b/src/class-tiny-image-size.php index 2148ab69..e174cf95 100644 --- a/src/class-tiny-image-size.php +++ b/src/class-tiny-image-size.php @@ -242,7 +242,7 @@ public function duplicate_of_size() { /** * Deletes the converted image file for this image size. - * + * * @return void */ public function delete_converted_image_size() { @@ -255,7 +255,11 @@ public function delete_converted_image_size() { if ( $convert_real_path && - Tiny_Helpers::str_starts_with( $convert_real_path, trailingslashit( $real_basedir ) ) ) { + Tiny_Helpers::str_starts_with( + $convert_real_path, + trailingslashit( $real_basedir ) + ) + ) { unlink( $convert_real_path ); } }