WebP-eXpress/lib/classes/SanityCheck.php
Malin 37cf714058 WebP Express CloudHost.es Fix v0.25.9-cloudhost
 Fixed bulk conversion getting stuck on missing files
 Added robust error handling and timeout protection
 Improved JavaScript response parsing
 Added file existence validation
 Fixed missing PHP class imports
 Added comprehensive try-catch error recovery

🔧 Key fixes:
- File existence checks before conversion attempts
- 30-second timeout protection per file
- Graceful handling of 500 errors and JSON parsing issues
- Automatic continuation to next file on failures
- Cache busting for JavaScript updates

🎯 Result: Bulk conversion now completes successfully even with missing files

🚀 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 10:22:32 +02:00

413 lines
13 KiB
PHP

<?php
namespace WebPExpress;
use \WebPExpress\PathHelper;
use \WebPExpress\Sanitize;
use \WebPExpress\SanityException;
class SanityCheck
{
private static function fail($errorMsg, $input)
{
// sanitize input before calling error_log(), it might be sent to file, mail, syslog etc.
//error_log($errorMsg . '. input:' . Sanitize::removeNUL($input) . 'backtrace: ' . print_r(debug_backtrace(), true));
error_log($errorMsg . '. input:' . Sanitize::removeNUL($input));
//error_log(get_magic_quotes_gpc() ? 'on' :'off');
throw new SanityException($errorMsg); // . '. Check debug.log for details (and make sure debugging is enabled)'
}
/**
*
* @param string $input string to test for NUL char
*/
public static function mustBeString($input, $errorMsg = 'String expected')
{
if (gettype($input) !== 'string') {
self::fail($errorMsg, $input);
}
return $input;
}
/**
* The NUL character is a demon, because it can be used to bypass other tests
* See https://st-g.de/2011/04/doing-filename-checks-securely-in-PHP.
*
* @param string $input string to test for NUL char
*/
public static function noNUL($input, $errorMsg = 'NUL character is not allowed')
{
self::mustBeString($input);
if (strpos($input, chr(0)) !== false) {
self::fail($errorMsg, $input);
}
return $input;
}
/**
* Prevent control chararters (#00 - #20).
*
* This prevents line feed, new line, tab, charater return, tab, ets.
* https://www.rapidtables.com/code/text/ascii-table.html
*
* @param string $input string to test for control characters
*/
public static function noControlChars($input, $errorMsg = 'Control characters are not allowed')
{
self::mustBeString($input);
self::noNUL($input);
if (preg_match('#[\x{0}-\x{1f}]#', $input)) {
self::fail($errorMsg, $input);
}
return $input;
}
/**
*
* @param mixed $input something that may not be empty
*/
public static function notEmpty($input, $errorMsg = 'Must be non-empty')
{
if (empty($input)) {
self::fail($errorMsg, '');
}
return $input;
}
public static function noDirectoryTraversal($input, $errorMsg = 'Directory traversal is not allowed')
{
self::mustBeString($input);
self::noControlChars($input);
if (preg_match('#\.\.\/#', $input)) {
self::fail($errorMsg, $input);
}
return $input;
}
public static function noStreamWrappers($input, $errorMsg = 'Stream wrappers are not allowed')
{
self::mustBeString($input);
self::noControlChars($input);
// Prevent stream wrappers ("phar://", "php://" and the like)
// https://www.php.net/manual/en/wrappers.phar.php
if (preg_match('#^\\w+://#', Sanitize::removeNUL($input))) {
self::fail($errorMsg, $input);
}
return $input;
}
public static function pathDirectoryTraversalAllowed($input)
{
self::notEmpty($input);
self::mustBeString($input);
self::noControlChars($input);
self::noStreamWrappers($input);
// PS: The following sanitize has no effect, as we have just tested that there are no NUL and
// no stream wrappers. It is here to avoid false positives on coderisk.com
$input = Sanitize::path($input);
return $input;
}
public static function pathWithoutDirectoryTraversal($input)
{
self::pathDirectoryTraversalAllowed($input);
self::noDirectoryTraversal($input);
$input = Sanitize::path($input);
return $input;
}
public static function path($input)
{
return self::pathWithoutDirectoryTraversal($input);
}
/**
* Beware: This does not take symlinks into account.
* I should make one that does. Until then, you should probably not call this method from outside this class
*/
private static function pathBeginsWith($input, $beginsWith, $errorMsg = 'Path is outside allowed path')
{
self::path($input);
if (!(strpos($input, $beginsWith) === 0)) {
self::fail($errorMsg, $input);
}
return $input;
}
private static function pathBeginsWithSymLinksExpanded($input, $beginsWith, $errorMsg = 'Path is outside allowed path') {
$closestExistingFolder = PathHelper::findClosestExistingFolderSymLinksExpanded($input);
self::pathBeginsWith($closestExistingFolder, $beginsWith, $errorMsg);
}
private static function absPathMicrosoftStyle($input, $errorMsg = 'Not an fully qualified Windows path')
{
// On microsoft we allow [drive letter]:\
if (!preg_match("#^[A-Z]:\\\\|/#", $input)) {
self::fail($errorMsg, $input);
}
return $input;
}
private static function isOnMicrosoft()
{
if (isset($_SERVER['SERVER_SOFTWARE'])) {
if (strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'microsoft') !== false) {
return true;
}
}
switch (PHP_OS) {
case "WINNT":
case "WIN32":
case "INTERIX":
case "UWIN":
case "UWIN-W7":
return true;
break;
}
return false;
}
public static function absPath($input, $errorMsg = 'Not an absolute path')
{
// first make sure there are no nasty things like control chars, phar wrappers, etc.
// - and no directory traversal either.
self::path($input);
// For non-windows, we require that an absolute path begins with "/"
// On windows, we also accept that a path starts with a drive letter, ie "C:\"
if ((strpos($input, '/') !== 0)) {
if (self::isOnMicrosoft()) {
self::absPathMicrosoftStyle($input);
} else {
self::fail($errorMsg, $input);
}
}
return $input;
}
public static function absPathInOneOfTheseRoots()
{
}
/**
* Look if filepath is within a dir path.
* Also tries expanding symlinks
*
* @param string $filePath Path to file. It may be non-existing.
* @param string $dirPath Path to dir. It must exist in order for symlinks to be expanded.
*/
private static function isFilePathWithinExistingDirPath($filePath, $dirPath)
{
// sanity-check input. It must be a valid absolute filepath. It is allowed to be non-existing
self::absPath($filePath);
// sanity-check dir and that it exists.
self::absPathExistsAndIsDir($dirPath);
return PathHelper::isFilePathWithinDirPath($filePath, $dirPath);
}
/**
* Look if filepath is within multiple dir paths.
* Also tries expanding symlinks
*
* @param string $input Path to file. It may be non-existing.
* @param array $roots Allowed root dirs. Note that they must exist in order for symlinks to be expanded.
*/
public static function filePathWithinOneOfTheseRoots($input, $roots, $errorMsg = 'The path is outside allowed roots.')
{
self::absPath($input);
foreach ($roots as $root) {
if (self::isFilePathWithinExistingDirPath($input, $root)) {
return $input;
}
}
self::fail($errorMsg, $input);
}
/*
public static function sourcePath($input, $errorMsg = 'The source path is outside allowed roots. It is only allowed to convert images that resides in: home dir, content path, upload dir and plugin dir.')
{
$validPaths = [
Paths::getHomeDirAbs(),
Paths::getIndexDirAbs(),
Paths::getContentDirAbs(),
Paths::getUploadDirAbs(),
Paths::getPluginDirAbs()
];
return self::filePathWithinOneOfTheseRoots($input, $validPaths, $errorMsg);
}
public static function destinationPath($input, $errorMsg = 'The destination path is outside allowed roots. The webps may only be stored in the upload folder and in the folder that WebP Express stores converted images in')
{
self::absPath($input);
// Webp Express only store converted images in upload folder and in its "webp-images" folder
// Check that destination path is within one of these.
$validPaths = [
'/var/www/webp-express-tests/we1'
//Paths::getUploadDirAbs(),
//Paths::getWebPExpressContentDirRel() . '/webp-images'
];
return self::filePathWithinOneOfTheseRoots($input, $validPaths, $errorMsg);
}*/
/**
* Test that path is an absolute path and it is in document root.
*
* If DOCUMENT_ROOT is not available, then only the absPath check will be done.
*
* TODO: Instead of this method, we shoud check
*
*
* It is acceptable if the absolute path does not exist
*/
public static function absPathIsInDocRoot($input, $errorMsg = 'Path is outside document root')
{
self::absPath($input);
if (!isset($_SERVER["DOCUMENT_ROOT"])) {
return $input;
}
if ($_SERVER["DOCUMENT_ROOT"] == '') {
return $input;
}
$docRoot = self::absPath($_SERVER["DOCUMENT_ROOT"]);
$docRoot = rtrim($docRoot, '/');
try {
$docRoot = self::absPathExistsAndIsDir($docRoot);
} catch (SanityException $e) {
return $input;
}
// Use realpath to expand symbolic links and check if it exists
$docRootSymLinksExpanded = @realpath($docRoot);
if ($docRootSymLinksExpanded === false) {
// probably outside open basedir restriction.
//$errorMsg = 'Cannot resolve document root';
//self::fail($errorMsg, $input);
// Cannot resolve document root, so cannot test if in document root
return $input;
}
// See if $filePath begins with the realpath of the $docRoot + '/'. If it does, we are done and OK!
// (pull #429)
if (strpos($input, $docRootSymLinksExpanded . '/') === 0) {
return $input;
}
$docRootSymLinksExpanded = rtrim($docRootSymLinksExpanded, '\\/');
$docRootSymLinksExpanded = self::absPathExists($docRootSymLinksExpanded, 'Document root does not exist!');
$docRootSymLinksExpanded = self::absPathExistsAndIsDir($docRootSymLinksExpanded, 'Document root is not a directory!');
$directorySeparator = self::isOnMicrosoft() ? '\\' : '/';
$errorMsg = 'Path is outside resolved document root (' . $docRootSymLinksExpanded . ')';
self::pathBeginsWithSymLinksExpanded($input, $docRootSymLinksExpanded . $directorySeparator, $errorMsg);
return $input;
}
public static function absPathExists($input, $errorMsg = 'Path does not exist or it is outside restricted basedir')
{
self::absPath($input);
if (@!file_exists($input)) {
// TODO: We might be able to detect if the problem is that the path does not exist or if the problem
// is that it is outside restricted basedir.
// ie by creating an error handler or inspecting the php ini "open_basedir" setting
self::fail($errorMsg, $input);
}
return $input;
}
public static function absPathExistsAndIsDir(
$input,
$errorMsg = 'Path points to a file (it should point to a directory)'
) {
self::absPathExists($input, 'Directory does not exist or is outside restricted basedir');
if (!is_dir($input)) {
self::fail($errorMsg, $input);
}
return $input;
}
public static function absPathExistsAndIsFile(
$input,
$errorMsg = 'Path points to a directory (it should not do that)'
) {
self::absPathExists($input, 'File does not exist or is outside restricted basedir');
if (@is_dir($input)) {
self::fail($errorMsg, $input);
}
return $input;
}
public static function absPathExistsAndIsFileInDocRoot($input)
{
self::absPathExistsAndIsFile($input);
self::absPathIsInDocRoot($input);
return $input;
}
public static function absPathExistsAndIsNotDir(
$input,
$errorMsg = 'Path points to a directory (it should point to a file)'
) {
self::absPathExistsAndIsFile($input, $errorMsg);
return $input;
}
public static function pregMatch($pattern, $input, $errorMsg = 'Does not match expected pattern')
{
self::noNUL($input);
self::mustBeString($input);
if (!preg_match($pattern, $input)) {
self::fail($errorMsg, $input);
}
return $input;
}
public static function isJSONArray($input, $errorMsg = 'Not a JSON array')
{
self::noNUL($input);
self::mustBeString($input);
self::notEmpty($input);
if ((strpos($input, '[') !== 0) || (!is_array(json_decode($input)))) {
self::fail($errorMsg, $input);
}
return $input;
}
public static function isJSONObject($input, $errorMsg = 'Not a JSON object')
{
self::noNUL($input);
self::mustBeString($input);
self::notEmpty($input);
if ((strpos($input, '{') !== 0) || (!is_object(json_decode($input)))) {
self::fail($errorMsg, $input);
}
return $input;
}
}