@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve error and large file handling in thumbnailing

Summary:
Ref T2479, T4406. We should do a better job of (a) handling image processing errors and (b) declining to process large image files.

This fixes the worst of it, which is that users can upload huge GIFs with a large number of frames and hang a `convert` process for a long time, eating a CPU and a pile of memory.

This code is still pretty iffy and needs some more work. A near-term product goal for it is supporting 100x100 profile images.

Test Plan: Uploaded large and small GIFs, after setting the definition of "enormous" to be pretty small. Saw the small GIFs thumbnail into animated GIFs, and the large ones thumbnail into static images.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T2479, T4406

Differential Revision: https://secure.phabricator.com/D8536

+206 -51
+206 -51
src/applications/files/PhabricatorImageTransformer.php
··· 1 1 <?php 2 2 3 + /** 4 + * @task enormous Detecting Enormous Images 5 + * @task save Saving Image Data 6 + */ 3 7 final class PhabricatorImageTransformer { 4 8 5 9 public function executeMemeTransform( ··· 231 235 232 236 return $scale; 233 237 } 238 + 234 239 private function generatePreview(PhabricatorFile $file, $size) { 235 240 $data = $file->loadFileData(); 236 241 $src = imagecreatefromstring($data); ··· 392 397 ); 393 398 } 394 399 395 - public static function saveImageDataInAnyFormat($data, $preferred_mime = '') { 396 - switch ($preferred_mime) { 397 - case 'image/gif': // Gif doesn't support true color 398 - ob_start(); 399 - imagegif($data); 400 - return ob_get_clean(); 401 - break; 402 - case 'image/png': 403 - if (function_exists('imagepng')) { 404 - ob_start(); 405 - imagepng($data, null, 9); 406 - return ob_get_clean(); 407 - } 408 - break; 409 - } 410 - 411 - $img = null; 412 - 413 - if (function_exists('imagejpeg')) { 414 - ob_start(); 415 - imagejpeg($data); 416 - $img = ob_get_clean(); 417 - } else if (function_exists('imagepng')) { 418 - ob_start(); 419 - imagepng($data, null, 9); 420 - $img = ob_get_clean(); 421 - } else if (function_exists('imagegif')) { 422 - ob_start(); 423 - imagegif($data); 424 - $img = ob_get_clean(); 425 - } else { 426 - throw new Exception("No image generation functions exist!"); 427 - } 428 - 429 - return $img; 430 - } 431 - 432 400 private function applyScaleWithImagemagick(PhabricatorFile $file, $dx, $dy) { 433 - 434 401 $img_type = $file->getMimeType(); 435 402 $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick'); 436 403 ··· 444 411 $x = imagesx($src); 445 412 $y = imagesy($src); 446 413 414 + if (self::isEnormousGIF($x, $y)) { 415 + return null; 416 + } 417 + 447 418 $scale = min(($dx / $x), ($dy / $y), 1); 448 419 449 420 $sdx = $scale * $x; ··· 454 425 455 426 $resized = new TempFile(); 456 427 457 - list($err) = exec_manual( 458 - 'convert %s -coalesce -resize %sX%s\! %s' 459 - , $input, $sdx, $sdy, $resized); 428 + $future = new ExecFuture( 429 + 'convert %s -coalesce -resize %sX%s%s %s', 430 + $input, 431 + $sdx, 432 + $sdy, 433 + '!', 434 + $resized); 460 435 461 - if (!$err) { 462 - $new_data = Filesystem::readFile($resized); 463 - return $new_data; 464 - } else { 465 - return null; 466 - } 436 + // Don't spend more than 10 seconds resizing; just fail if it takes longer 437 + // than that. 438 + $future->setTimeout(10)->resolvex(); 467 439 440 + return Filesystem::readFile($resized); 468 441 } 469 442 470 443 private function applyMemeWithImagemagick( ··· 475 448 $img_type) { 476 449 477 450 $output = new TempFile(); 478 - 479 - execx('convert %s -coalesce +adjoin %s_%%09d', 451 + $future = new ExecFuture( 452 + 'convert %s -coalesce +adjoin %s_%s', 453 + $input, 480 454 $input, 481 - $input); 455 + '%09d'); 456 + $future->setTimeout(10)->resolvex(); 482 457 458 + $output_files = array(); 483 459 for ($ii = 0; $ii < $count; $ii++) { 484 460 $frame_name = sprintf('%s_%09d', $input, $ii); 485 461 $output_name = sprintf('%s_%09d', $output, $ii); 486 462 463 + $output_files[] = $output_name; 464 + 487 465 $frame_data = Filesystem::readFile($frame_name); 488 466 $memed_frame_data = $this->applyMemeTo( 489 467 $frame_data, ··· 493 471 Filesystem::writeFile($output_name, $memed_frame_data); 494 472 } 495 473 496 - execx('convert -loop 0 %s_* %s', $output, $output); 474 + $future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output); 475 + $future->setTimeout(10)->resolvex(); 497 476 498 477 return Filesystem::readFile($output); 499 478 } 479 + 480 + /* -( Detecting Enormous Files )------------------------------------------- */ 481 + 482 + 483 + /** 484 + * Determine if an image is enormous (too large to transform). 485 + * 486 + * Attackers can perform a denial of service attack by uploading highly 487 + * compressible images with enormous dimensions but a very small filesize. 488 + * Transforming them (e.g., into thumbnails) may consume huge quantities of 489 + * memory and CPU relative to the resources required to transmit the file. 490 + * 491 + * In general, we respond to these images by declining to transform them, and 492 + * using a default thumbnail instead. 493 + * 494 + * @param int Width of the image, in pixels. 495 + * @param int Height of the image, in pixels. 496 + * @return bool True if this image is enormous (too large to transform). 497 + * @task enormous 498 + */ 499 + public static function isEnormousImage($x, $y) { 500 + // This is just a sanity check, but if we don't have valid dimensions we 501 + // shouldn't be trying to transform the file. 502 + if (($x <= 0) || ($y <= 0)) { 503 + return true; 504 + } 505 + 506 + return ($x * $y) > (4096 * 4096); 507 + } 508 + 509 + 510 + /** 511 + * Determine if a GIF is enormous (too large to transform). 512 + * 513 + * For discussion, see @{method:isEnormousImage}. We need to be more 514 + * careful about GIFs, because they can also have a large number of frames 515 + * despite having a very small filesize. We're more conservative about 516 + * calling GIFs enormous than about calling images in general enormous. 517 + * 518 + * @param int Width of the GIF, in pixels. 519 + * @param int Height of the GIF, in pixels. 520 + * @return bool True if this image is enormous (too large to transform). 521 + * @task enormous 522 + */ 523 + public static function isEnormousGIF($x, $y) { 524 + if (self::isEnormousImage($x, $y)) { 525 + return true; 526 + } 527 + 528 + return ($x * $y) > (800 * 800); 529 + } 530 + 531 + 532 + /* -( Saving Image Data )-------------------------------------------------- */ 533 + 534 + 535 + /** 536 + * Save an image resource to a string representation suitable for storage or 537 + * transmission as an image file. 538 + * 539 + * Optionally, you can specify a preferred MIME type like `"image/png"`. 540 + * Generally, you should specify the MIME type of the original file if you're 541 + * applying file transformations. The MIME type may not be honored if 542 + * Phabricator can not encode images in the given format (based on available 543 + * extensions), but can save images in another format. 544 + * 545 + * @param resource GD image resource. 546 + * @param string? Optionally, preferred mime type. 547 + * @return string Bytes of an image file. 548 + * @task save 549 + */ 550 + public static function saveImageDataInAnyFormat($data, $preferred_mime = '') { 551 + $preferred = null; 552 + switch ($preferred_mime) { 553 + case 'image/gif': 554 + $preferred = self::saveImageDataAsGIF($data); 555 + break; 556 + case 'image/png': 557 + $preferred = self::saveImageDataAsPNG($data); 558 + break; 559 + } 560 + 561 + if ($preferred !== null) { 562 + return $preferred; 563 + } 564 + 565 + $data = self::saveImageDataAsJPG($data); 566 + if ($data !== null) { 567 + return $data; 568 + } 569 + 570 + $data = self::saveImageDataAsPNG($data); 571 + if ($data !== null) { 572 + return $data; 573 + } 574 + 575 + $data = self::saveImageDataAsGIF($data); 576 + if ($data !== null) { 577 + return $data; 578 + } 579 + 580 + throw new Exception(pht('Failed to save image data into any format.')); 581 + } 582 + 583 + 584 + /** 585 + * Save an image in PNG format, returning the file data as a string. 586 + * 587 + * @param resource GD image resource. 588 + * @return string|null PNG file as a string, or null on failure. 589 + * @task save 590 + */ 591 + private static function saveImageDataAsPNG($image) { 592 + if (!function_exists('imagepng')) { 593 + return null; 594 + } 595 + 596 + ob_start(); 597 + $result = imagepng($image, null, 9); 598 + $output = ob_get_clean(); 599 + 600 + if (!$result) { 601 + return null; 602 + } 603 + 604 + return $output; 605 + } 606 + 607 + 608 + /** 609 + * Save an image in GIF format, returning the file data as a string. 610 + * 611 + * @param resource GD image resource. 612 + * @return string|null GIF file as a string, or null on failure. 613 + * @task save 614 + */ 615 + private static function saveImageDataAsGIF($image) { 616 + if (!function_exists('imagegif')) { 617 + return null; 618 + } 619 + 620 + ob_start(); 621 + $result = imagegif($image); 622 + $output = ob_get_clean(); 623 + 624 + if (!$result) { 625 + return null; 626 + } 627 + 628 + return $output; 629 + } 630 + 631 + 632 + /** 633 + * Save an image in JPG format, returning the file data as a string. 634 + * 635 + * @param resource GD image resource. 636 + * @return string|null JPG file as a string, or null on failure. 637 + * @task save 638 + */ 639 + private static function saveImageDataAsJPG($image) { 640 + if (!function_exists('imagejpeg')) { 641 + return null; 642 + } 643 + 644 + ob_start(); 645 + $result = imagejpeg($image); 646 + $output = ob_get_clean(); 647 + 648 + if (!$result) { 649 + return null; 650 + } 651 + 652 + return $output; 653 + } 654 + 500 655 501 656 }