VYPR
Moderate severityNVD Advisory· Published Mar 4, 2021· Updated Feb 25, 2026

[20210308] - Core - Path Traversal within joomla/archive zip class

CVE-2021-26028

Description

An issue was discovered in Joomla! 3.0.0 through 3.9.24. Extracting an specifilcy crafted zip package could write files outside of the intended path.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
joomla/archivePackagist
< 1.1.101.1.10

Affected products

1

Patches

1
32c9009a1020

Security - Ensure unpacked files stay below destination path

https://github.com/joomla-framework/archiveNiels BraczekMar 6, 2021via ghsa
2 files changed · +43 17
  • composer.json+1 1 modified
    @@ -7,7 +7,7 @@
         "license": "GPL-2.0-or-later",
         "require": {
             "php": "^5.3.10|~7.0",
    -        "joomla/filesystem": "~1.3|~2.0@beta"
    +        "joomla/filesystem": "~1.6|~2.0@beta"
         },
         "require-dev": {
             "joomla/coding-standards": "~2.0@alpha",
    
  • src/Zip.php+42 16 modified
    @@ -204,7 +204,7 @@ public static function hasNativeSupport()
     	 *
     	 * @since   1.0
     	 */
    -	public function checkZipData(&$data)
    +	public function checkZipData($data)
     	{
     		return strpos($data, $this->fileHeader) !== false;
     	}
    @@ -237,24 +237,29 @@ protected function extractCustom($archive, $destination)
     			throw new \RuntimeException('Get ZIP Information failed');
     		}
     
    -		for ($i = 0, $n = \count($this->metadata); $i < $n; $i++)
    +		foreach ($this->metadata as $i => $metadata)
     		{
    -			$lastPathCharacter = substr($this->metadata[$i]['name'], -1, 1);
    +			$lastPathCharacter = substr($metadata['name'], -1, 1);
     
     			if ($lastPathCharacter !== '/' && $lastPathCharacter !== '\\')
     			{
     				$buffer = $this->getFileData($i);
    -				$path   = Path::clean($destination . '/' . $this->metadata[$i]['name']);
    +				$path   = Path::clean($destination . '/' . $metadata['name']);
    +
    +				if (!$this->isBelow($destination, $destination . '/' . $metadata['name']))
    +				{
    +					throw new \RuntimeException('Unable to write outside of destination path', 100);
    +				}
     
     				// Make sure the destination folder exists
     				if (!Folder::create(\dirname($path)))
     				{
    -					throw new \RuntimeException('Unable to create destination folder ' . \dirname($path));
    +					throw new \RuntimeException('Unable to create destination folder');
     				}
     
     				if (!File::write($path, $buffer))
     				{
    -					throw new \RuntimeException('Unable to write entry to file ' . $path);
    +					throw new \RuntimeException('Unable to write file');
     				}
     			}
     		}
    @@ -305,6 +310,11 @@ protected function extractNative($archive, $destination)
     				throw new \RuntimeException('Unable to read ZIP entry');
     			}
     
    +			if (!$this->isBelow($destination, $destination . '/' . $file))
    +			{
    +				throw new \RuntimeException('Unable to write outside of destination path', 100);
    +			}
    +
     			if (File::write($destination . '/' . $file, $buffer) === false)
     			{
     				throw new \RuntimeException('Unable to write ZIP entry to file ' . $destination . '/' . $file);
    @@ -322,13 +332,13 @@ protected function extractNative($archive, $destination)
     	 * <pre>
     	 * KEY: Position in zipfile
     	 * VALUES: 'attr'  --  File attributes
    -	 * 'crc'   --  CRC checksum
    -	 * 'csize' --  Compressed file size
    -	 * 'date'  --  File modification time
    -	 * 'name'  --  Filename
    -	 * 'method'--  Compression method
    -	 * 'size'  --  Original file size
    -	 * 'type'  --  File type
    +	 *         'crc'   --  CRC checksum
    +	 *         'csize' --  Compressed file size
    +	 *         'date'  --  File modification time
    +	 *         'name'  --  Filename
    +	 *         'method'--  Compression method
    +	 *         'size'  --  Original file size
    +	 *         'type'  --  File type
     	 * </pre>
     	 *
     	 * @param   string  $data  The ZIP archive buffer.
    @@ -338,7 +348,7 @@ protected function extractNative($archive, $destination)
     	 * @since   1.0
     	 * @throws  \RuntimeException
     	 */
    -	private function readZipInfo(&$data)
    +	private function readZipInfo($data)
     	{
     		$entries = array();
     
    @@ -546,7 +556,7 @@ private function addToZipFile(array &$file, array &$contents, array &$ctrldir)
     		$uncLen = \strlen($data);
     		$crc    = crc32($data);
     		$zdata  = gzcompress($data);
    -		$zdata  = substr(substr($zdata, 0, \strlen($zdata) - 4), 2);
    +		$zdata  = substr(substr($zdata, 0, -4), 2);
     		$cLen   = \strlen($zdata);
     
     		// CRC 32 information.
    @@ -643,7 +653,7 @@ private function addToZipFile(array &$file, array &$contents, array &$ctrldir)
     	 * @since   1.0
     	 * @todo	Review and finish implementation
     	 */
    -	private function createZipFile(array &$contents, array &$ctrlDir, $path)
    +	private function createZipFile(array $contents, array $ctrlDir, $path)
     	{
     		$data = implode('', $contents);
     		$dir  = implode('', $ctrlDir);
    @@ -665,4 +675,20 @@ private function createZipFile(array &$contents, array &$ctrlDir, $path)
     
     		return File::write($path, $buffer);
     	}
    +
    +	/**
    +	 * Check if a path is below a given destination path
    +	 *
    +	 * @param   string  $destination
    +	 * @param   string  $path
    +	 *
    +	 * @return  boolean
    +	 */
    +	private function isBelow($destination, $path)
    +	{
    +		$absoluteRoot = Path::clean(Path::resolve($destination));
    +		$absolutePath = Path::clean(Path::resolve($path));
    +
    +		return strpos($absolutePath, $absoluteRoot) === 0;
    +	}
     }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.