name : uploadshield.php
<?php
/**
 * @package   admintools
 * @copyright Copyright (c)2010-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

defined('_JEXEC') || die;

class AtsystemFeatureUploadshield extends AtsystemFeatureAbstract
{
	protected $loadOrder = 370;

	/**
	 * Is this feature enabled?
	 *
	 * @return bool
	 */
	public function isEnabled()
	{
		if (!$this->container->platform->isFrontend())
		{
			return false;
		}

		if ($this->skipFiltering)
		{
			return false;
		}

		return ($this->cparams->getValue('uploadshield', 1) == 1);
	}

	/**
	 * Scans all uploaded files for PHP tags. This prevents uploading PHP files or crafted
	 * images with raw PHP code in them which may lead to arbitrary code execution under
	 * several common circumstances. It will also block files with null bytes in their
	 * filenames or with double extensions which include PHP in them (e.g. .php.jpg).
	 */
	public function onAfterInitialise()
	{
		// Do we have uploaded files?
		$input = $this->input->files;

		$ref = new ReflectionProperty($input, 'data');
		$ref->setAccessible(true);
		$filesHash = $ref->getValue($input);

		if (empty($filesHash))
		{
			return;
		}

		$extraInfo = '';

		foreach ($filesHash as $key => $temp_descriptor)
		{
			if (is_array($temp_descriptor) && !array_key_exists('tmp_name', $temp_descriptor))
			{
				$descriptors = $temp_descriptor;
			}
			else
			{
				$descriptors[] = $temp_descriptor;
			}

			unset($temp_descriptor);

			foreach ($descriptors as $descriptor)
			{
				$files = [];

				if (is_array($descriptor['tmp_name']))
				{
					foreach ($descriptor['tmp_name'] as $key => $value)
					{
						$files[] = [
							'name'     => $descriptor['name'][$key],
							'type'     => $descriptor['type'][$key],
							'tmp_name' => $descriptor['tmp_name'][$key],
							'error'    => $descriptor['error'][$key],
							'size'     => $descriptor['size'][$key],
						];
					}
				}
				else
				{
					$files[] = $descriptor;
				}

				foreach ($files as $fileDescriptor)
				{
					$tempNames     = $fileDescriptor['tmp_name'];
					$intendedNames = $fileDescriptor['name'];

					if (!is_array($tempNames))
					{
						$tempNames = [$tempNames];
					}

					if (!is_array($intendedNames))
					{
						$intendedNames = [$intendedNames];
					}

					$len = count($tempNames);

					for ($i = 0; $i < $len; $i++)
					{
						$tempName     = array_shift($tempNames);
						$intendedName = array_shift($intendedNames);

						$extraInfo = "File descriptor :\n";
						$extraInfo .= print_r($fileDescriptor, true);
						$extraInfo .= "\n";

						// Empty file name (ie the field has been submitted, but it has no actual value in here), simply skip
						if (!$tempName)
						{
							continue;
						}

						// 1. Null byte check
						if (strstr($intendedName, "\u0000"))
						{
							$extraInfo .= "Block reason: null byte\n";

							$this->exceptionsHandler->blockRequest('uploadshield', null, $extraInfo);

							return;
						}

						// 2. PHP-in-extension check
						$explodedName = explode('.', $intendedName);
						$explodedName = array_reverse($explodedName);

						// 2a. File extension is .php
						if ((count($explodedName) > 1) && (strtolower($explodedName[0]) == 'php'))
						{
							$extraInfo .= "Block reason: file extension is .php\n";

							$this->exceptionsHandler->blockRequest('uploadshield', null, $extraInfo);

							return;
						}

						// 2a. File extension is php.xxx
						if ((count($explodedName) > 2) && (strtolower($explodedName[1]) == 'php'))
						{
							$extraInfo .= "Block reason: file extension is in the form of .php.xxx\n";

							$this->exceptionsHandler->blockRequest('uploadshield', null, $extraInfo);

							return;
						}

						// 2b. File extensions is php.xxx.yyy
						if ((count($explodedName) > 3) && (strtolower($explodedName[2]) == 'php'))
						{
							$extraInfo .= "Block reason: file extension is in the form of .php.xxx.yyy\n";

							$this->exceptionsHandler->blockRequest('uploadshield', null, $extraInfo);

							return;
						}

						// For whatever reason we can't access file contents, skip it
						if (!@file_exists($tempName) || !@is_readable($tempName))
						{
							continue;
						}

						// 3. Contents scanner
						$fp = @fopen($tempName, 'r');

						if ($fp === false)
						{
							continue;
						}

						// Initialise
						$data                          = '';
						$extension                     = strtolower($explodedName[0]);
						$possibleFileForShortTagSyntax = in_array($extension, [
							'inc', 'phps', 'class', 'php3', 'php4', 'txt', 'dat', 'tpl', 'tmpl',
						]);

						// Process the file in 128Kb chunks
						while (!feof($fp))
						{
							// Read 128Kb and add it to the existing data (the last 4 bytes of the previous scan)
							$buffer = @fread($fp, 131072);
							$data   .= $buffer;

							// Do we have a regular PHP tag?
							if (stristr($buffer, '<?php'))
							{
								$extraInfo .= "Block reason: file contains PHP open tag\n";

								$this->exceptionsHandler->blockRequest('uploadshield', null, $extraInfo);

								return;
							}

							// Do we have a (possibly concealed) PHAR file?
							if (strstr($buffer, '__HALT_COMPILER'))
							{
								$extraInfo .= "Block reason: file is a possibly concealed PHAR file\n";

								$this->exceptionsHandler->blockRequest('uploadshield', null, $extraInfo);

								return;
							}

							// If we have text file which may have the short tag (<?) in it...
							if ($possibleFileForShortTagSyntax)
							{
								// ...do I have a short tag?
								if (strstr($buffer, '<?'))
								{
									$extraInfo .= "Block reason: file contains PHP short tag\n";

									$this->exceptionsHandler->blockRequest('uploadshield', null, $extraInfo);

									return;
								}
							}

							// Keep the last 4 bytes of data to make sure we can catch partial strings.
							$data = substr($data, -14);

							// WARNING: Do NOT try seek to an earlier position! Here's how it all works.
							//
							// We just need to keep the last four bytes in $data so we can append the next 128Kb.
							// This way if the start of the tag is in the previous block and the rest is in the next
							// 128Kb block we can still scan it. The value 14 is not random. __HALT_COMPILER is 15 characters
							// and the longest string we're trying to detect. If it existed in this block we'd have
							// already found it and blocked it. Therefore the only possibility is that this block
							// ended in any of the first 14 characters of that substring.
							//
							// Do NOT seek to an earlier file position. It would be a rather silly thing to do.
						}

						fclose($fp);
					}
				}
			}
		}
	}
}

© 2025 Cubjrnet7