Merge commit from fork

* Test Protocol Not Supplied Before Using is_file

* added missing assertFile and renamed prohibitPhar

* More Tests

---------

Co-authored-by: calligraf0 <115566010+calligraf0@users.noreply.github.com>
This commit is contained in:
oleibman
2026-04-04 21:04:58 -07:00
committed by GitHub
parent 36013a3db5
commit 93c94eb31d
5 changed files with 81 additions and 10 deletions
+7 -10
View File
@@ -31,19 +31,16 @@ class Downloader
public function __construct(string $folder, string $filename, ?string $filetype = null)
{
if ((is_dir($folder) === false) || (is_readable($folder) === false)) {
throw new Exception('Folder is not accessible');
}
$filepath = "{$folder}/{$filename}";
$this->filepath = (string) realpath($filepath);
$this->filename = basename($filepath);
clearstatcache();
if ((is_file($this->filepath) === false) || (is_readable($this->filepath) === false)) {
throw new Exception('File not found, or not a regular file, or cannot be read');
$filepath = realpath("{$folder}/{$filename}");
if ($filepath === false || !is_file($filepath) || !is_readable($filepath)) {
throw new Exception('File not found, or cannot be read');
}
$this->filepath = $filepath;
$this->filename = basename($this->filepath);
$filetype ??= pathinfo($filename, PATHINFO_EXTENSION);
if (array_key_exists(strtolower($filetype), self::CONTENT_TYPES) === false) {
$filetype ??= pathinfo($this->filename, PATHINFO_EXTENSION);
if (!array_key_exists(strtolower($filetype), self::CONTENT_TYPES)) {
throw new Exception('Invalid filetype: file cannot be downloaded');
}
$this->filetype = strtolower($filetype);
+12
View File
@@ -285,4 +285,16 @@ abstract class IOFactory
self::$readers[$readerType] = $readerClass;
}
/**
* @return array<string, class-string<IReader>>
*
* @internal
*
* @codeCoverageIgnore
*/
public static function getReaders(): array
{
return self::$readers;
}
}
+1
View File
@@ -93,6 +93,7 @@ class Xml extends BaseReader
];
// Open file
File::assertFile($filename);
$data = (string) file_get_contents($filename);
$data = $this->getSecurityScannerOrThrow()->scan($data);
+21
View File
@@ -133,11 +133,30 @@ class File
return tempnam(self::sysGetTempDir(), 'phpspreadsheet') ?: throw new Exception('Could not create temporary file');
}
/**
* All filenames starting with protocol (e.g. phar://) are prohibited.
* Note that many protocols, including http and zip, will already
* return false for is_file.
* A whitelist of protocols may be added if needed in future.
*/
public static function prohibitWrappers(string $filename): void
{
$scheme = parse_url($filename, PHP_URL_SCHEME);
// strlen check > 1 to avoid issues with Windows absolute paths (e.g. C:\...), Windows quirks :)
// since no built-in or commonly registered PHP stream wrapper uses a single-character scheme, this should be ok, to my knowledge
if (is_string($scheme) && strlen($scheme) > 1) {
throw new Exception(
"Stream wrappers are not permitted as file paths: {$filename}"
);
}
}
/**
* Assert that given path is an existing file and is readable, otherwise throw exception.
*/
public static function assertFile(string $filename, string $zipMember = ''): void
{
self::prohibitWrappers($filename);
if (!is_file($filename) || !is_readable($filename)) {
throw new ReaderException('File "' . $filename . '" does not exist or is not readable.');
}
@@ -156,9 +175,11 @@ class File
/**
* Same as assertFile, except return true/false and don't throw Exception.
* Will nevertheless throw if filename uses invalid protocol, e.g. phar.
*/
public static function testFileNoThrow(string $filename, ?string $zipMember = null): bool
{
self::prohibitWrappers($filename);
if (!is_file($filename) || !is_readable($filename)) {
return false;
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Reader;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Reader\IReader;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class NoPharTest extends TestCase
{
/**
* @param class-string<IReader> $reader
*/
#[DataProvider('providerReaders')]
public function testNoPhar(string $reader): void
{
$this->expectException(SpreadsheetException::class);
$this->expectExceptionMessage('Stream wrappers are not permitted');
$reader = new $reader();
$reader->load('phar://anyoldname');
}
/**
* @return array<array<class-string<IReader>>>
*/
public static function providerReaders(): array
{
$readers = IOFactory::getReaders();
$array = [];
foreach ($readers as $key => $reader) {
$array[$key] = [$reader];
}
return $array;
}
}