mirror of
https://github.com/e107inc/e107.git
synced 2025-01-17 20:58:30 +01:00
413 lines
13 KiB
PHP
413 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* e107 website system
|
|
*
|
|
* Copyright (C) 2008-2020 e107 Inc (e107.org)
|
|
* Released under the terms and conditions of the
|
|
* GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
|
|
*/
|
|
|
|
require_once("e_file_inspector_interface.php");
|
|
|
|
/**
|
|
* File Inspector
|
|
*
|
|
* Tool to validate application files for consistency by comparing hashes of files with those in a database
|
|
*/
|
|
abstract class e_file_inspector implements e_file_inspector_interface
|
|
{
|
|
protected $database;
|
|
protected $currentVersion;
|
|
private $validatedBitmask;
|
|
|
|
protected $defaultDirsCache;
|
|
protected $customDirsCache;
|
|
private $undeterminable = array();
|
|
|
|
// FIXME: Better place for the insecure file list
|
|
public $insecureFiles = [
|
|
e_ADMIN . "ad_links.php",
|
|
e_PLUGIN . "tinymce4/e_meta.php",
|
|
e_THEME . "bootstrap3/css/bootstrap_dark.css",
|
|
e_PLUGIN . "search_menu/languages/English.php",
|
|
e_LANGUAGEDIR . e_LANGUAGE . "/lan_parser_functions.php",
|
|
e_LANGUAGEDIR . e_LANGUAGE . "/admin/help/theme.php",
|
|
e_HANDLER . "np_class.php",
|
|
e_CORE . "shortcodes/single/user_extended.sc",
|
|
e_ADMIN . "download.php",
|
|
e_PLUGIN . "banner/config.php",
|
|
e_PLUGIN . "forum/newforumposts_menu_config.php",
|
|
e_PLUGIN . "forum/e_latest.php",
|
|
e_PLUGIN . "forum/e_status.php",
|
|
e_PLUGIN . "forum/forum_post_shortcodes.php",
|
|
e_PLUGIN . "forum/forum_shortcodes.php",
|
|
e_PLUGIN . "forum/forum_update_check.php",
|
|
e_PLUGIN . "online_extended_menu/online_extended_menu.php",
|
|
e_PLUGIN . "online_extended_menu/images/user.png",
|
|
e_PLUGIN . "online_extended_menu/languages/English.php",
|
|
e_PLUGIN . "pm/sendpm.sc",
|
|
e_PLUGIN . "pm/shortcodes/",
|
|
e_PLUGIN . "social/e_header.php",
|
|
];
|
|
|
|
private $existingInsecureFiles = array();
|
|
private $existingInsecureDirectories = array();
|
|
|
|
/**
|
|
* @var null|resource File pointer resource opened in write mode or null if not initialized yet
|
|
*/
|
|
private $deprecatedLogFileHandle = null;
|
|
|
|
/**
|
|
* e_file_inspector constructor
|
|
* @param string $database The database from which integrity data may be read or to which integrity data may be
|
|
* written. This should be an URL or absolute file path for most implementations.
|
|
*/
|
|
public function __construct($database)
|
|
{
|
|
$this->database = $database;
|
|
|
|
$appRoot = e107::getInstance()->file_path;
|
|
$this->undeterminable = array_map(function ($path)
|
|
{
|
|
return realpath($path) ? realpath($path) : $path;
|
|
}, [
|
|
$appRoot . "e107_config.php",
|
|
$appRoot . e107::getFolder('system_base') . "core_image.phar",
|
|
]
|
|
);
|
|
$this->existingInsecureFiles = array_filter($this->insecureFiles, function ($path)
|
|
{
|
|
return is_file($path);
|
|
});
|
|
$this->existingInsecureFiles = array_map('realpath', $this->existingInsecureFiles);
|
|
$this->existingInsecureDirectories = array_filter($this->insecureFiles, function ($path)
|
|
{
|
|
return is_dir($path);
|
|
});
|
|
$this->existingInsecureDirectories = array_map('realpath', $this->existingInsecureDirectories);
|
|
}
|
|
|
|
/**
|
|
* Get a list of deprecated files cached in deprecatedFiles.log from the last File Inspector scan
|
|
* @return array
|
|
*/
|
|
public static function getCachedDeprecatedFiles()
|
|
{
|
|
$log = e_LOG.'fileinspector/deprecatedFiles.log';
|
|
|
|
if(!file_exists($log))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
$content = file_get_contents($log);
|
|
|
|
if(empty($content))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
$tmp = explode("\n", $content);
|
|
$cachedDeprecatedFiles = [];
|
|
foreach($tmp as $line)
|
|
{
|
|
if(empty($line))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$cachedDeprecatedFiles[] = e_BASE.$line;
|
|
}
|
|
|
|
return $cachedDeprecatedFiles;
|
|
}
|
|
|
|
/**
|
|
* Convert validation code to string.
|
|
* @param $validationCode
|
|
* @return string
|
|
*/
|
|
public static function getStatusForValidationCode($validationCode)
|
|
{
|
|
$status = 'unknown';
|
|
if ($validationCode & self::VALIDATED)
|
|
$status = 'check';
|
|
elseif (!($validationCode & self::VALIDATED_FILE_EXISTS))
|
|
$status = 'missing';
|
|
elseif (!($validationCode & self::VALIDATED_FILE_SECURITY))
|
|
$status = 'warning';
|
|
elseif (!($validationCode & self::VALIDATED_PATH_KNOWN))
|
|
$status = 'unknown';
|
|
elseif (!($validationCode & self::VALIDATED_PATH_VERSION))
|
|
$status = 'old';
|
|
elseif (!($validationCode & self::VALIDATED_HASH_CALCULABLE))
|
|
$status = 'uncalc';
|
|
elseif (!($validationCode & self::VALIDATED_HASH_CURRENT))
|
|
if ($validationCode & self::VALIDATED_HASH_EXISTS)
|
|
$status = 'old';
|
|
else
|
|
$status = 'fail';
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* Prepare the provided database for reading or writing
|
|
*
|
|
* Should tolerate a non-existent database and try to create it if a write operation is executed.
|
|
*
|
|
* @return void
|
|
*/
|
|
abstract public function loadDatabase();
|
|
|
|
/**
|
|
* Check the integrity of the provided path
|
|
*
|
|
* @param $path string Relative path of the file to look up
|
|
* @param $version string The desired software release to match.
|
|
* Leave blank for the current version.
|
|
* Do not prepend the version number with "v".
|
|
* @return int Validation code (see the constants of this class)
|
|
*/
|
|
public function validate($path, $version = null)
|
|
{
|
|
if ($version === null) $version = $this->getCurrentVersion();
|
|
|
|
$bits = 0x0;
|
|
$absolutePath = $this->relativePathToAbsolutePath($path);
|
|
$dbChecksums = $this->getChecksums($path);
|
|
$dbChecksum = $this->getChecksum($path, $version);
|
|
$actualChecksum = !empty($dbChecksums) ? $this->checksumPath($absolutePath) : null;
|
|
|
|
if (!empty($dbChecksums)) $bits |= self::VALIDATED_PATH_KNOWN;
|
|
if ($dbChecksum !== false) $bits |= self::VALIDATED_PATH_VERSION;
|
|
if (file_exists($absolutePath)) $bits |= self::VALIDATED_FILE_EXISTS;
|
|
if (!$this->isInsecure($path)) $bits |= self::VALIDATED_FILE_SECURITY;
|
|
if ($this->isDeterminable($absolutePath)) $bits |= self::VALIDATED_HASH_CALCULABLE;
|
|
if ($actualChecksum === $dbChecksum) $bits |= self::VALIDATED_HASH_CURRENT;
|
|
|
|
foreach ($dbChecksums as $dbChecksum)
|
|
{
|
|
if ($dbChecksum === $actualChecksum) $bits |= self::VALIDATED_HASH_EXISTS;
|
|
}
|
|
|
|
if ($bits + self::VALIDATED === $this->getValidatedBitmask()) $bits |= self::VALIDATED;
|
|
|
|
$this->log($path, $bits);
|
|
|
|
return $bits;
|
|
}
|
|
|
|
/**
|
|
* Log old file paths. (may be expanded to other types in future)
|
|
*
|
|
* @param string $relativePath
|
|
* @param int $status
|
|
* @return null
|
|
*/
|
|
private function log($relativePath, $status)
|
|
{
|
|
if(empty($relativePath) || self::getStatusForValidationCode($status) !== 'old') // deprecated-file status
|
|
{
|
|
return null;
|
|
}
|
|
|
|
$message = $relativePath."\n";
|
|
|
|
if (!is_resource($this->deprecatedLogFileHandle))
|
|
{
|
|
$logPath = e_LOG."fileinspector/";
|
|
|
|
if(!is_dir($logPath))
|
|
{
|
|
mkdir($logPath, 0775);
|
|
}
|
|
|
|
$this->deprecatedLogFileHandle = fopen($logPath."deprecatedFiles.log", 'w');
|
|
}
|
|
|
|
fwrite($this->deprecatedLogFileHandle, $message);
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the file integrity hash for the provided path and version
|
|
*
|
|
* @param $path string Relative path of the file to look up
|
|
* @param $version string The software release version corresponding to the file hash.
|
|
* Leave blank for the current version.
|
|
* Do not prepend the version number with "v".
|
|
* @return string|bool The database hash for the path and version specified. FALSE if the record does not exist.
|
|
*/
|
|
public function getChecksum($path, $version = null)
|
|
{
|
|
if ($version === null) $version = $this->getCurrentVersion();
|
|
$checksums = $this->getChecksums($path);
|
|
return isset($checksums[$version]) ? $checksums[$version] : false;
|
|
}
|
|
|
|
/**
|
|
* Calculate the hash of a path to compare with the hash database
|
|
*
|
|
* @param $absolutePath string Absolute path of the file to hash
|
|
* @return string|bool The actual hash for the path. FALSE if the hash was incalculable.
|
|
*/
|
|
public function checksumPath($absolutePath)
|
|
{
|
|
if (!$this->isDeterminable($absolutePath)) return false;
|
|
|
|
return $this->checksum(file_get_contents($absolutePath));
|
|
}
|
|
|
|
/**
|
|
* Calculate the hash of a string, which would be used to compare with the hash database
|
|
*
|
|
* @param $content string Full content to hash
|
|
* @return string
|
|
*/
|
|
public function checksum($content)
|
|
{
|
|
return md5(str_replace(array(chr(13), chr(10)), "", $content));
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function getVersions($path)
|
|
{
|
|
return array_keys($this->getChecksums($path));
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function getCurrentVersion()
|
|
{
|
|
if ($this->currentVersion) return $this->currentVersion;
|
|
|
|
$checksums = $this->getChecksums("index.php");
|
|
$versions = array_keys($checksums);
|
|
usort($versions, 'version_compare');
|
|
return $this->currentVersion = array_pop($versions);
|
|
}
|
|
|
|
/**
|
|
* Get the matching version of the provided path
|
|
*
|
|
* Useful for looking up the versions of old files that no longer exist in the latest image
|
|
*
|
|
* @param $path string Relative path of the file to look up
|
|
* @return string|bool PHP-standardized version of the file. FALSE if there is no match.
|
|
*/
|
|
public function getVersion($path)
|
|
{
|
|
$actualChecksum = $this->checksumPath($path);
|
|
foreach ($this->getChecksums($path) as $dbVersion => $dbChecksum)
|
|
{
|
|
if ($actualChecksum === $dbChecksum) return $dbVersion;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function isInsecure($path)
|
|
{
|
|
$absolutePath = $this->relativePathToAbsolutePath($path);
|
|
if (in_array($absolutePath, $this->existingInsecureFiles)) return true;
|
|
foreach ($this->existingInsecureDirectories as $existingInsecureDirectory)
|
|
{
|
|
$existingInsecureDirectory .= '/';
|
|
if (substr($absolutePath, 0, strlen($existingInsecureDirectory)) === $existingInsecureDirectory) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Convert a custom site path to a default path
|
|
* @param string $path Custom path
|
|
* @return string
|
|
*/
|
|
public function customPathToDefaultPath($path)
|
|
{
|
|
if (!is_array($this->customDirsCache)) $this->populateDirsCache();
|
|
foreach ($this->customDirsCache as $dirType => $customDir)
|
|
{
|
|
if (!isset($this->defaultDirsCache[$dirType])) continue;
|
|
|
|
$defaultDir = $this->defaultDirsCache[$dirType];
|
|
if ($customDir === $defaultDir) continue;
|
|
|
|
if (substr($path, 0, strlen($customDir)) === $customDir)
|
|
$path = $defaultDir . substr($path, strlen($customDir));
|
|
}
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* @param $path
|
|
* @return mixed|string
|
|
*/
|
|
public function defaultPathToCustomPath($path)
|
|
{
|
|
if (!is_array($this->customDirsCache)) $this->populateDirsCache();
|
|
foreach ($this->customDirsCache as $dirType => $customDir)
|
|
{
|
|
if (!isset($this->defaultDirsCache[$dirType])) continue;
|
|
|
|
$defaultDir = $this->defaultDirsCache[$dirType];
|
|
if ($customDir === $defaultDir) continue;
|
|
|
|
if (substr($path, 0, strlen($defaultDir)) === $defaultDir)
|
|
$path = $customDir . substr($path, strlen($defaultDir));
|
|
}
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* @return int
|
|
*/
|
|
private function getValidatedBitmask()
|
|
{
|
|
if ($this->validatedBitmask !== null) return $this->validatedBitmask;
|
|
$constants = (new ReflectionClass(self::class))->getConstants();
|
|
$validated_constants = array_filter($constants, function ($key)
|
|
{
|
|
$str = 'VALIDATED_';
|
|
return substr($key, 0, strlen($str)) === $str;
|
|
}, ARRAY_FILTER_USE_KEY);
|
|
|
|
$this->validatedBitmask = (max($validated_constants) << 0x1) - 0x1;
|
|
return $this->validatedBitmask;
|
|
}
|
|
|
|
/**
|
|
* @param $absolutePath
|
|
* @return bool
|
|
*/
|
|
private function isDeterminable($absolutePath)
|
|
{
|
|
return is_file($absolutePath) && is_readable($absolutePath) && !in_array($absolutePath, $this->undeterminable);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
protected function populateDirsCache()
|
|
{
|
|
$this->defaultDirsCache = e107::getInstance()->defaultDirs();
|
|
$customDirs = e107::getInstance()->e107_dirs ? e107::getInstance()->e107_dirs : [];
|
|
$this->customDirsCache = array_diff_assoc($customDirs, $this->defaultDirsCache);
|
|
}
|
|
|
|
/**
|
|
* @param $path
|
|
* @return false|string
|
|
*/
|
|
private function relativePathToAbsolutePath($path)
|
|
{
|
|
return realpath(e_BASE . $path);
|
|
}
|
|
} |