Skip to content

Commit

Permalink
Merge pull request from GHSA-6363-v5m4-fvq3
Browse files Browse the repository at this point in the history
* fix: Add fix for security advisory GHSA-6363-v5m4-fvq3

* fix: Add check for WebP

* fix: Allow http and https protocols

* fix: Add check for is_animated_gif()
  • Loading branch information
gchtr committed Apr 10, 2024
1 parent 8ae9747 commit f8f2e5a
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 4 deletions.
5 changes: 5 additions & 0 deletions lib/Image.php
Expand Up @@ -128,6 +128,11 @@ protected function get_dimensions( $dim ) {
if ( isset($this->_dimensions) ) {
return $this->get_dimensions_loaded($dim);
}

if (!ImageHelper::is_protocol_allowed($this->file_loc) ) {
throw new \InvalidArgumentException('The output file scheme is not supported.');
}

if ( file_exists($this->file_loc) && filesize($this->file_loc) ) {
if ( ImageHelper::is_svg( $this->file_loc ) ) {
$svg_size = $this->svgs_get_dimensions( $this->file_loc );
Expand Down
5 changes: 4 additions & 1 deletion lib/Image/Operation/ToJpg.php
Expand Up @@ -42,11 +42,14 @@ public function filename( $src_filename, $src_extension = 'jpg' ) {
* @return bool true if everything went fine, false otherwise
*/
public function run( $load_filename, $save_filename ) {
if (!ImageHelper::is_protocol_allowed($load_filename) ) {
throw new \InvalidArgumentException('The output file scheme is not supported.');
}

if ( !file_exists($load_filename) ) {
return false;
}

// Attempt to check if SVG.
if ( ImageHelper::is_svg($load_filename) ) {
return false;
Expand Down
6 changes: 5 additions & 1 deletion lib/Image/Operation/ToWebp.php
Expand Up @@ -7,7 +7,7 @@
use Timber\ImageHelper;

/**
* This class is used to process webp images. Not all server configurations support webp.
* This class is used to process webp images. Not all server configurations support webp.
* If webp is not enabled, Timber will generate webp images instead
* @codeCoverageIgnore
*/
Expand Down Expand Up @@ -42,6 +42,10 @@ public function filename( $src_filename, $src_extension = 'webp' ) {
* @return bool true if everything went fine, false otherwise
*/
public function run( $load_filename, $save_filename ) {
if (!ImageHelper::is_protocol_allowed($load_filename)) {
throw new \InvalidArgumentException('The output file scheme is not supported.');
}

if (!is_file($load_filename)) {
return false;
}
Expand Down
61 changes: 59 additions & 2 deletions lib/ImageHelper.php
Expand Up @@ -32,6 +32,10 @@ class ImageHelper {

static $home_url;

protected const ALLOWED_PROTOCOLS = ['file', 'http', 'https'];

protected const WINDOWS_LOCAL_FILENAME_REGEX = '/^[a-z]:(?:[\\\\\/]?(?:[\w\s!#()-]+|[\.]{1,2})+)*[\\\\\/]?/i';

public static function init() {
self::$home_url = get_home_url();
add_action('delete_attachment', array(__CLASS__, 'delete_attachment'));
Expand Down Expand Up @@ -121,6 +125,11 @@ public static function is_animated_gif( $file ) {
//doesn't have .gif, bail
return false;
}

if (!ImageHelper::is_protocol_allowed($file) ) {
throw new \InvalidArgumentException('The output file scheme is not supported.');
}

//its a gif so test
if ( !($fh = @fopen($file, 'rb')) ) {
return false;
Expand Down Expand Up @@ -150,7 +159,15 @@ public static function is_animated_gif( $file ) {
* @return bool True if SVG, false if not SVG or file doesn't exist.
*/
public static function is_svg( $file_path ) {
if ( ! isset( $file_path ) || '' === $file_path || ! file_exists( $file_path ) ) {
if ( ! isset( $file_path ) || '' === $file_path ) {
return false;
}

if (!ImageHelper::is_protocol_allowed($file_path) ) {
throw new \InvalidArgumentException('The output file scheme is not supported.');
}

if ( ! file_exists( $file_path ) ) {
return false;
}

Expand Down Expand Up @@ -366,6 +383,11 @@ public static function get_sideloaded_file_loc( $file ) {
*/
public static function sideload_image( $file ) {
$loc = self::get_sideloaded_file_loc($file);

if (!ImageHelper::is_protocol_allowed($file) ) {
throw new \InvalidArgumentException('The output file scheme is not supported.');
}

if ( file_exists($loc) ) {
return URLHelper::file_system_to_url($loc);
}
Expand Down Expand Up @@ -576,12 +598,16 @@ private static function _operate( $src, $op, $force = false ) {
return '';
}

if (!ImageHelper::is_protocol_allowed($src) ) {
throw new \InvalidArgumentException('The output file scheme is not supported.');
}

$allow_fs_write = apply_filters('timber/allow_fs_write', true);

if ( $allow_fs_write === false ) {
return $src;
}

$external = false;
// if external image, load it first
if ( URLHelper::is_external_content($src) ) {
Expand Down Expand Up @@ -682,4 +708,35 @@ public static function get_resize_file_path( $url, $w, $h, $crop ) {
);
return $new_path;
}

/**
* Checks if the protocol of the given filename is allowed.
*
* This fixes a security issue with a PHAR deserialization vulnerability
* with file_exists() in PHP < 8.0.0.
*
* @param string $filepath File path.
* @return bool
*/
public static function is_protocol_allowed($filepath) {
$parsed_url = \parse_url($filepath);

if (false === $parsed_url) {
throw new \InvalidArgumentException('The filename is not valid.');
}

$protocol = isset($parsed_url['scheme'])
? \mb_strtolower($parsed_url['scheme'])
: 'file';

if (
\PHP_OS_FAMILY === 'Windows'
&& \strlen($protocol) === 1
&& \preg_match(self::WINDOWS_LOCAL_FILENAME_REGEX, $filepath)
) {
$protocol = 'file';
}

return \in_array($protocol, self::ALLOWED_PROTOCOLS, true);
}
}
44 changes: 44 additions & 0 deletions tests/test-timber-image.php
Expand Up @@ -1167,4 +1167,48 @@ function testSVGDimensions() {
$this->assertEquals( 20, $image->height() );
}

function testPharProtocolIsNotAllowedwithResize() {
$object = new ImageOperation\Resize( 400, 300, 'center' );
$load_filename = 'phar://test.jpg';
$save_filename = 'test-new.jpg';

$this->expectException( InvalidArgumentException::class );
$object->run( $load_filename, $save_filename );
}

function testPharProtocolIsNotAllowedwithLetterbox() {
$object = new ImageOperation\Letterbox( 400, 300, '#FFFFFF' );
$load_filename = 'phar://test.jpg';
$save_filename = 'test-new.jpg';

$this->expectException( InvalidArgumentException::class );
$object->run( $load_filename, $save_filename );
}

function testPharProtocolIsNotAllowedwithRetina() {
$object = new ImageOperation\Retina( 2 );
$load_filename = 'phar://test.jpg';
$save_filename = 'test-new.jpg';

$this->expectException( InvalidArgumentException::class );
$object->run( $load_filename, $save_filename );
}

function testPharProtocolIsNotAllowedwithToJpg() {
$object = new ImageOperation\ToJpg( '#FFFFFF' );
$load_filename = 'phar://test.svg';
$save_filename = 'test-new.svg';

$this->expectException( InvalidArgumentException::class );
$object->run( $load_filename, $save_filename );
}

function testPharProtocolIsNotAllowedwithToWebp() {
$object = new ImageOperation\ToWebp( 80 );
$load_filename = 'phar://test.png';
$save_filename = 'test-new.png';

$this->expectException( InvalidArgumentException::class );
$object->run( $load_filename, $save_filename );
}
}

0 comments on commit f8f2e5a

Please sign in to comment.