PHPSpreadsheet has a patch bypass for CVE-2026-34084
Description
PhpSpreadsheet's patch bypass allows phar wrapper deserialization, leading to RCE on PHP 7.x and file read on PHP 8.x.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
PhpSpreadsheet's patch bypass allows phar wrapper deserialization, leading to RCE on PHP 7.x and file read on PHP 8.x.
Vulnerability
A bypass vulnerability exists in PhpSpreadsheet's File::prohibitWrappers helper function, which was intended to prevent stream wrappers like phar://. The function uses parse_url to check for schemes, but inputs like phar:///path/file.phar/inner cause parse_url to return false instead of the scheme. This skips the security check, allowing the IOFactory::load method to process the phar wrapper. This affects versions of PhpSpreadsheet where the File::prohibitWrappers function is present and vulnerable, as described in references [1], [2], and [3].
Exploitation
An attacker needs to provide a specially crafted file path to IOFactory::load. The path should use the phar:// wrapper with three or more slashes after the scheme, such as phar:///path/to/attacker.phar/file.csv. When this path is processed, the bypassed prohibitWrappers check allows the application to attempt to load the phar file. On PHP 7.x, simply accessing the phar wrapper via is_file triggers automatic metadata deserialization. On PHP 8.x, RCE requires a downstream consumer to call Phar::getMetadata after the phar wrapper is read [2, 3].
Impact
On PHP 7.x, successful exploitation allows an attacker to achieve Remote Code Execution (RCE) by triggering the deserialization of malicious phar metadata, which can invoke magic methods like __wakeup and __destruct [1, 2, 3]. On PHP 8.x, the impact is reduced to a file read primitive of the phar wrapper, with RCE only possible if the application further processes the phar metadata [2, 3].
Mitigation
This vulnerability was patched in PhpSpreadsheet. The exact patched version and release date are not explicitly stated in the provided references, but the advisory GHSA-87m4-826x-3crx indicates a fix is available [2, 3]. Users should update to the latest version of PhpSpreadsheet to ensure the patch is applied. No workarounds are mentioned in the available references.
AI Insight generated on Jun 8, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: <=5.7.0, <=5.6.0, <=3.10.5, <=2.4.5, <=2.1.16, <=1.30.4
Patches
527230e16d2feSecurity Patch
6 files changed · +80 −46
CHANGELOG.md+5 −1 modified@@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org). This is a ## 2026-04-09 - 3.10.5 +### Security Note + +- File::prohibitWrappers and Drawing::setPath now reject phar paths with extra leading slashes (e.g. phar:///…) that escaped the prior parse_url-based filter. No security exploit was possible even with the extra slashes. Backport of [PR #4876](https://github.com/PHPOffice/PhpSpreadsheet/pull/4876) + ### Fixed -- Security patches. +- Third-party security patches. ## 2026-04-09 - 3.10.4
composer.lock+33 −33 modified@@ -1346,16 +1346,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -1398,9 +1398,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "paragonie/random_compat", @@ -2065,16 +2065,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.58", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -2095,7 +2095,7 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.4", + "sebastian/comparator": "^5.0.5", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.4", @@ -2146,7 +2146,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -2170,7 +2170,7 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:04:46+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "psr/container", @@ -3139,16 +3139,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -3204,7 +3204,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { @@ -3224,7 +3224,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T05:25:07+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", @@ -3924,27 +3924,27 @@ }, { "name": "setasign/fpdi", - "version": "v2.6.4", + "version": "v2.6.7", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "4b53852fde2734ec6a07e458a085db627c60eada" + "reference": "388c51e69982a3fc16698710b763e8107a49f510" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada", - "reference": "4b53852fde2734ec6a07e458a085db627c60eada", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/388c51e69982a3fc16698710b763e8107a49f510", + "reference": "388c51e69982a3fc16698710b763e8107a49f510", "shasum": "" }, "require": { "ext-zlib": "*", - "php": "^7.1 || ^8.0" + "php": ">=7.2 <=8.5.99999" }, "conflict": { "setasign/tfpdf": "<1.31" }, "require-dev": { - "phpunit/phpunit": "^7", + "phpunit/phpunit": "^8.5.52", "setasign/fpdf": "~1.8.6", "setasign/tfpdf": "~1.33", "squizlabs/php_codesniffer": "^3.5", @@ -3984,15 +3984,15 @@ ], "support": { "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.6.4" + "source": "https://github.com/Setasign/FPDI/tree/v2.6.7" }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", "type": "tidelift" } ], - "time": "2025-08-05T09:57:14+00:00" + "time": "2026-05-13T10:16:22+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -5109,16 +5109,16 @@ }, { "name": "symfony/process", - "version": "v6.4.25", + "version": "v6.4.39", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8" + "reference": "6c93071cb8c91dce5a41960d125e019e64ef6cb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", - "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", + "url": "https://api.github.com/repos/symfony/process/zipball/6c93071cb8c91dce5a41960d125e019e64ef6cb5", + "reference": "6c93071cb8c91dce5a41960d125e019e64ef6cb5", "shasum": "" }, "require": { @@ -5150,7 +5150,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.25" + "source": "https://github.com/symfony/process/tree/v6.4.39" }, "funding": [ { @@ -5170,7 +5170,7 @@ "type": "tidelift" } ], - "time": "2025-08-14T06:23:17+00:00" + "time": "2026-05-11T16:53:15+00:00" }, { "name": "symfony/service-contracts",
src/PhpSpreadsheet/Shared/File.php+10 −6 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use ZipArchive; @@ -139,19 +140,22 @@ public static function temporaryFilename(): string } /** - * All filenames starting with protocol (e.g. phar://) are prohibited. + * Blocks phar:// and similar RCE-bearing wrappers. * 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. + * data: is intentionally allowed; callers needing strict + * on-disk-only semantics must validate $filename themselves. */ 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) { + if ( + Preg::IsMatch('~^phar://~i', $filename) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $filename) && !Preg::isMatch('/^([\w.]+):/', $filename)) + || Preg::isMatch('~^[\w.]+://.*phar:~is', $filename) + ) { throw new Exception( - "Stream wrappers are not permitted as file paths: {$filename}" + "Disallowed stream wrapper used for {$filename}" ); } }
src/PhpSpreadsheet/Worksheet/Drawing.php+8 −3 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use ZipArchive; @@ -96,7 +97,7 @@ public function getPath(): string public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip = null, bool $allowExternal = true, ?callable $isWhitelisted = null): static { $this->isUrl = false; - if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) { + if (Preg::isMatch('~^data:image/[a-z]+;base64,~', $path)) { $this->path = $path; return $this; @@ -113,8 +114,12 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip } } // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 - } elseif (filter_var($path, FILTER_VALIDATE_URL) || (preg_match('/^([\w\s\x00-\x1f]+):/u', $path) && !preg_match('/^([\w]+):/u', $path))) { - if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) { + } elseif ( + filter_var($path, FILTER_VALIDATE_URL) + || Preg::isMatch('~^phar://~i', $path) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $path) && !Preg::isMatch('/^([\w.]+):/', $path)) + ) { + if (!Preg::isMatch('/^(http|https|file|ftp|s3):/', $path)) { throw new PhpSpreadsheetException('Invalid protocol for linked drawing'); } if (!$allowExternal) {
tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php+4 −0 modified@@ -157,7 +157,11 @@ public static function providerBadProtocol(): array 'mailto' => ['mailto:xyz@example.com'], 'mailto whitespace' => ['mail to:xyz@example.com'], 'phar' => ['phar://example.com/image.phar'], + 'phar with 3 slashes' => ['phar:///example.com/image.phar'], 'phar control' => ["\x14phar://example.com/image.phar"], + 'filter with phar' => ['php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar'], + 'protocol with period followed by phar' => ['compress.zlib://phar:///x.phar'], + 'protocol with period and embedded space' => ['comp ress.zlib://anything'], ]; } }
tests/PhpSpreadsheetTests/Reader/NoPharTest.php+20 −3 modified@@ -18,10 +18,27 @@ class NoPharTest extends TestCase #[DataProvider('providerReaders')] public function testNoPhar(string $reader): void { - $this->expectException(SpreadsheetException::class); - $this->expectExceptionMessage('Stream wrappers are not permitted'); + $invalidProtocol = [ + 'normal phar' => 'phar://anyoldname', + '3 slashes' => 'phar:///anyoldname', + 'mixed case' => 'Phar:///anyoldname', + 'embedded space' => 'ph ar://anyoldname', + 'leading space' => ' phar://anyoldname', + 'embedded control character' => "ph\x04ar://anyoldname", + 'filter with phar' => 'php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar', + 'filter with phar and newline' => "php://filter/read=convert.base64-encode/\nresource=phar:///tmp/x.Phar", + 'protocol with period followed by phar' => 'compress.bzip2://phar:///x.phar', + 'protocol with period and embedded space' => 'comp ress.zlib://anything', + ]; $reader = new $reader(); - $reader->load('phar://anyoldname'); + foreach ($invalidProtocol as $key => $value) { + try { + $reader->load($value); + self::fail("Should have thrown exception - $key"); + } catch (SpreadsheetException $e) { + self::assertStringContainsString('Disallowed stream wrapper', $e->getMessage(), $key); + } + } } /**
9fb47c798dcbSecurity Patch
6 files changed · +80 −46
CHANGELOG.md+5 −1 modified@@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org). This is a ## 2026-04-19 - 2.4.5 +### Security Note + +- File::prohibitWrappers and Drawing::setPath now reject phar paths with extra leading slashes (e.g. phar:///…) that escaped the prior parse_url-based filter. No security exploit was possible even with the extra slashes. Backport of [PR #4876](https://github.com/PHPOffice/PhpSpreadsheet/pull/4876) + ### Fixed -- Security patches. +- Third-party security patches. ## 2026-04-09 - 2.4.4
composer.lock+33 −33 modified@@ -1350,16 +1350,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -1402,9 +1402,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "paragonie/random_compat", @@ -2069,16 +2069,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.58", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -2099,7 +2099,7 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.4", + "sebastian/comparator": "^5.0.5", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.4", @@ -2150,7 +2150,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -2174,7 +2174,7 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:04:46+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "psr/container", @@ -3143,16 +3143,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -3208,7 +3208,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { @@ -3228,7 +3228,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T05:25:07+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", @@ -3928,27 +3928,27 @@ }, { "name": "setasign/fpdi", - "version": "v2.6.4", + "version": "v2.6.7", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "4b53852fde2734ec6a07e458a085db627c60eada" + "reference": "388c51e69982a3fc16698710b763e8107a49f510" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada", - "reference": "4b53852fde2734ec6a07e458a085db627c60eada", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/388c51e69982a3fc16698710b763e8107a49f510", + "reference": "388c51e69982a3fc16698710b763e8107a49f510", "shasum": "" }, "require": { "ext-zlib": "*", - "php": "^7.1 || ^8.0" + "php": ">=7.2 <=8.5.99999" }, "conflict": { "setasign/tfpdf": "<1.31" }, "require-dev": { - "phpunit/phpunit": "^7", + "phpunit/phpunit": "^8.5.52", "setasign/fpdf": "~1.8.6", "setasign/tfpdf": "~1.33", "squizlabs/php_codesniffer": "^3.5", @@ -3988,15 +3988,15 @@ ], "support": { "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.6.4" + "source": "https://github.com/Setasign/FPDI/tree/v2.6.7" }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", "type": "tidelift" } ], - "time": "2025-08-05T09:57:14+00:00" + "time": "2026-05-13T10:16:22+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -5113,16 +5113,16 @@ }, { "name": "symfony/process", - "version": "v6.4.25", + "version": "v6.4.39", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8" + "reference": "6c93071cb8c91dce5a41960d125e019e64ef6cb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", - "reference": "6be2f0c9ab3428587c07bed03aa9e3d1b823c6c8", + "url": "https://api.github.com/repos/symfony/process/zipball/6c93071cb8c91dce5a41960d125e019e64ef6cb5", + "reference": "6c93071cb8c91dce5a41960d125e019e64ef6cb5", "shasum": "" }, "require": { @@ -5154,7 +5154,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.25" + "source": "https://github.com/symfony/process/tree/v6.4.39" }, "funding": [ { @@ -5174,7 +5174,7 @@ "type": "tidelift" } ], - "time": "2025-08-14T06:23:17+00:00" + "time": "2026-05-11T16:53:15+00:00" }, { "name": "symfony/service-contracts",
src/PhpSpreadsheet/Shared/File.php+10 −6 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use ZipArchive; @@ -139,19 +140,22 @@ public static function temporaryFilename(): string } /** - * All filenames starting with protocol (e.g. phar://) are prohibited. + * Blocks phar:// and similar RCE-bearing wrappers. * 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. + * data: is intentionally allowed; callers needing strict + * on-disk-only semantics must validate $filename themselves. */ 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) { + if ( + Preg::IsMatch('~^phar://~i', $filename) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $filename) && !Preg::isMatch('/^([\w.]+):/', $filename)) + || Preg::isMatch('~^[\w.]+://.*phar:~is', $filename) + ) { throw new Exception( - "Stream wrappers are not permitted as file paths: {$filename}" + "Disallowed stream wrapper: {$filename}" ); } }
src/PhpSpreadsheet/Worksheet/Drawing.php+8 −3 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use ZipArchive; @@ -96,7 +97,7 @@ public function getPath(): string public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip = null, bool $allowExternal = true, ?callable $isWhitelisted = null): static { $this->isUrl = false; - if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) { + if (Preg::isMatch('~^data:image/[a-z]+;base64,~', $path)) { $this->path = $path; return $this; @@ -113,8 +114,12 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip } } // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 - } elseif (filter_var($path, FILTER_VALIDATE_URL) || (preg_match('/^([\w\s\x00-\x1f]+):/u', $path) && !preg_match('/^([\w]+):/u', $path))) { - if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) { + } elseif ( + filter_var($path, FILTER_VALIDATE_URL) + || Preg::isMatch('~^phar://~i', $path) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $path) && !Preg::isMatch('/^([\w.]+):/', $path)) + ) { + if (!Preg::isMatch('/^(http|https|file|ftp|s3):/', $path)) { throw new PhpSpreadsheetException('Invalid protocol for linked drawing'); } if (!$allowExternal) {
tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php+4 −0 modified@@ -157,7 +157,11 @@ public static function providerBadProtocol(): array 'mailto' => ['mailto:xyz@example.com'], 'mailto whitespace' => ['mail to:xyz@example.com'], 'phar' => ['phar://example.com/image.phar'], + 'phar with 3 slashes' => ['phar:///example.com/image.phar'], 'phar control' => ["\x14phar://example.com/image.phar"], + 'filter with phar' => ['php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar'], + 'protocol with period followed by phar' => ['compress.zlib://phar:///x.phar'], + 'protocol with period and embedded space' => ['comp ress.zlib://anything'], ]; } }
tests/PhpSpreadsheetTests/Reader/NoPharTest.php+20 −3 modified@@ -18,10 +18,27 @@ class NoPharTest extends TestCase #[DataProvider('providerReaders')] public function testNoPhar(string $reader): void { - $this->expectException(SpreadsheetException::class); - $this->expectExceptionMessage('Stream wrappers are not permitted'); + $invalidProtocol = [ + 'normal phar' => 'phar://anyoldname', + '3 slashes' => 'phar:///anyoldname', + 'mixed case' => 'Phar:///anyoldname', + 'embedded space' => 'ph ar://anyoldname', + 'leading space' => ' phar://anyoldname', + 'embedded control character' => "ph\x04ar://anyoldname", + 'filter with phar' => 'php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar', + 'filter with phar and newline' => "php://filter/read=convert.base64-encode/\nresource=phar:///tmp/x.Phar", + 'protocol with period followed by phar' => 'compress.bzip2://phar:///x.phar', + 'protocol with period and embedded space' => 'comp ress.zlib://anything', + ]; $reader = new $reader(); - $reader->load('phar://anyoldname'); + foreach ($invalidProtocol as $key => $value) { + try { + $reader->load($value); + self::fail("Should have thrown exception - $key"); + } catch (SpreadsheetException $e) { + self::assertStringContainsString('Disallowed stream wrapper', $e->getMessage(), $key); + } + } } /**
ed3b50e158e8Security Patch Try 2
5 files changed · +46 −12
CHANGELOG.md+4 −0 modified@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). This is a ## 2026-04-19 - 2.1.16 +### Security Note + +- File::prohibitWrappers and Drawing::setPath now reject phar paths with extra leading slashes (e.g. phar:///…) that escaped the prior parse_url-based filter. No security exploit was possible even with the extra slashes. Backport of [PR #4876](https://github.com/PHPOffice/PhpSpreadsheet/pull/4876) + ### Fixed - Security patches.
src/PhpSpreadsheet/Shared/File.php+10 −6 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use ZipArchive; @@ -139,19 +140,22 @@ public static function temporaryFilename(): string } /** - * All filenames starting with protocol (e.g. phar://) are prohibited. + * Blocks phar:// and similar RCE-bearing wrappers. * 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. + * data: is intentionally allowed; callers needing strict + * on-disk-only semantics must validate $filename themselves. */ 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) { + if ( + Preg::IsMatch('~^phar://~i', $filename) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $filename) && !Preg::isMatch('/^([\w.]+):/', $filename)) + || Preg::isMatch('~^[\w.]+://.*phar:~is', $filename) + ) { throw new Exception( - "Stream wrappers are not permitted as file paths: {$filename}" + "Disallowed stream wrapper used for {$filename}" ); } }
src/PhpSpreadsheet/Worksheet/Drawing.php+8 −3 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use ZipArchive; @@ -96,7 +97,7 @@ public function getPath(): string public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip = null, bool $allowExternal = true, ?callable $isWhitelisted = null): static { $this->isUrl = false; - if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) { + if (Preg::isMatch('~^data:image/[a-z]+;base64,~', $path)) { $this->path = $path; return $this; @@ -113,8 +114,12 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip } } // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 - } elseif (filter_var($path, FILTER_VALIDATE_URL) || (preg_match('/^([\w\s\x00-\x1f]+):/u', $path) && !preg_match('/^([\w]+):/u', $path))) { - if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) { + } elseif ( + filter_var($path, FILTER_VALIDATE_URL) + || Preg::isMatch('~^phar://~i', $path) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $path) && !Preg::isMatch('/^([\w.]+):/', $path)) + ) { + if (!Preg::isMatch('/^(http|https|file|ftp|s3):/', $path)) { throw new PhpSpreadsheetException('Invalid protocol for linked drawing'); } if (!$allowExternal) {
tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php+4 −0 modified@@ -161,7 +161,11 @@ public static function providerBadProtocol(): array 'mailto' => ['mailto:xyz@example.com'], 'mailto whitespace' => ['mail to:xyz@example.com'], 'phar' => ['phar://example.com/image.phar'], + 'phar with 3 slashes' => ['phar:///example.com/image.phar'], 'phar control' => ["\x14phar://example.com/image.phar"], + 'filter with phar' => ['php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar'], + 'protocol with period followed by phar' => ['compress.zlib://phar:///x.phar'], + 'protocol with period and embedded space' => ['comp ress.zlib://anything'], ]; } }
tests/PhpSpreadsheetTests/Reader/NoPharTest.php+20 −3 modified@@ -18,10 +18,27 @@ class NoPharTest extends TestCase */ public function testNoPhar(string $reader): void { - $this->expectException(SpreadsheetException::class); - $this->expectExceptionMessage('Stream wrappers are not permitted'); + $invalidProtocol = [ + 'normal phar' => 'phar://anyoldname', + '3 slashes' => 'phar:///anyoldname', + 'mixed case' => 'Phar:///anyoldname', + 'embedded space' => 'ph ar://anyoldname', + 'leading space' => ' phar://anyoldname', + 'embedded control character' => "ph\x04ar://anyoldname", + 'filter with phar' => 'php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar', + 'filter with phar and newline' => "php://filter/read=convert.base64-encode/\nresource=phar:///tmp/x.Phar", + 'protocol with period followed by phar' => 'compress.bzip2://phar:///x.phar', + 'protocol with period and embedded space' => 'comp ress.zlib://anything', + ]; $reader = new $reader(); - $reader->load('phar://anyoldname'); + foreach ($invalidProtocol as $key => $value) { + try { + $reader->load($value); + self::fail("Should have thrown exception - $key"); + } catch (SpreadsheetException $e) { + self::assertStringContainsString('Disallowed stream wrapper', $e->getMessage(), $key); + } + } } /**
49d828bbd292Security Patch
7 files changed · +199 −106
CHANGELOG.md+5 −1 modified@@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org). This is a ## 2026-04-19 - 2.1.16 +### Security Note + +- File::prohibitWrappers and Drawing::setPath now reject phar paths with extra leading slashes (e.g. phar:///…) that escaped the prior parse_url-based filter. No security exploit was possible even with the extra slashes. + ### Fixed -- Security patches. +- Security patches, including backport PR #4876, and patches to 3rd party support software. ## 2026-04-09 - 2.1.15
composer.lock+139 −80 modified@@ -666,30 +666,29 @@ }, { "name": "doctrine/instantiator", - "version": "1.5.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.4" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", + "doctrine/coding-standard": "^14", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -716,7 +715,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" }, "funding": [ { @@ -732,7 +731,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2026-01-05T06:47:08+00:00" }, { "name": "dompdf/dompdf", @@ -1279,16 +1278,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -1327,28 +1326,28 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -1367,7 +1366,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -1391,9 +1390,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "paragonie/random_compat", @@ -2056,16 +2055,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { @@ -2076,7 +2075,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -2087,11 +2086,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.10", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -2139,7 +2138,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -2150,12 +2149,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "psr/container", @@ -2544,16 +2551,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -2606,15 +2613,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -2804,16 +2823,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -2869,28 +2888,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -2933,15 +2964,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -3114,16 +3157,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -3165,15 +3208,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -3340,27 +3395,27 @@ }, { "name": "setasign/fpdi", - "version": "v2.6.4", + "version": "v2.6.7", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "4b53852fde2734ec6a07e458a085db627c60eada" + "reference": "388c51e69982a3fc16698710b763e8107a49f510" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada", - "reference": "4b53852fde2734ec6a07e458a085db627c60eada", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/388c51e69982a3fc16698710b763e8107a49f510", + "reference": "388c51e69982a3fc16698710b763e8107a49f510", "shasum": "" }, "require": { "ext-zlib": "*", - "php": "^7.1 || ^8.0" + "php": ">=7.2 <=8.5.99999" }, "conflict": { "setasign/tfpdf": "<1.31" }, "require-dev": { - "phpunit/phpunit": "^7", + "phpunit/phpunit": "^8.5.52", "setasign/fpdf": "~1.8.6", "setasign/tfpdf": "~1.33", "squizlabs/php_codesniffer": "^3.5", @@ -3400,15 +3455,15 @@ ], "support": { "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.6.4" + "source": "https://github.com/Setasign/FPDI/tree/v2.6.7" }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", "type": "tidelift" } ], - "time": "2025-08-05T09:57:14+00:00" + "time": "2026-05-13T10:16:22+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -4481,16 +4536,16 @@ }, { "name": "symfony/process", - "version": "v5.4.46", + "version": "v5.4.51", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4" + "reference": "467bfc56f18f5ef6d5ccb09324d7e988c1c0a98f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/01906871cb9b5e3cf872863b91aba4ec9767daf4", - "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4", + "url": "https://api.github.com/repos/symfony/process/zipball/467bfc56f18f5ef6d5ccb09324d7e988c1c0a98f", + "reference": "467bfc56f18f5ef6d5ccb09324d7e988c1c0a98f", "shasum": "" }, "require": { @@ -4523,7 +4578,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.46" + "source": "https://github.com/symfony/process/tree/v5.4.51" }, "funding": [ { @@ -4534,12 +4589,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-06T09:18:28+00:00" + "time": "2026-01-26T15:53:37+00:00" }, { "name": "symfony/service-contracts", @@ -4845,16 +4904,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -4883,15 +4942,15 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { "url": "https://github.com/theseer", "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [],
.github/workflows/main.yml+13 −13 modified@@ -20,7 +20,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install locales run: sudo apt-get update && sudo apt-get install -y language-pack-fr language-pack-de @@ -37,7 +37,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -68,7 +68,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 2 @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 2 @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -120,7 +120,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -136,7 +136,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -151,7 +151,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -167,7 +167,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -182,7 +182,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -198,7 +198,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -213,7 +213,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -231,7 +231,7 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.ref }} # Otherwise our annotated tag is not fetched and we cannot get correct version
src/PhpSpreadsheet/Shared/File.php+10 −6 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use ZipArchive; @@ -139,19 +140,22 @@ public static function temporaryFilename(): string } /** - * All filenames starting with protocol (e.g. phar://) are prohibited. + * Blocks phar:// and similar RCE-bearing wrappers. * 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. + * data: is intentionally allowed; callers needing strict + * on-disk-only semantics must validate $filename themselves. */ 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) { + if ( + Preg::IsMatch('~^phar://~i', $filename) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $filename) && !Preg::isMatch('/^([\w.]+):/', $filename)) + || Preg::isMatch('~^[\w.]+://.*phar:~is', $filename) + ) { throw new Exception( - "Stream wrappers are not permitted as file paths: {$filename}" + "Disallowed stream wrapper used for {$filename}" ); } }
src/PhpSpreadsheet/Worksheet/Drawing.php+8 −3 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use ZipArchive; @@ -96,7 +97,7 @@ public function getPath(): string public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip = null, bool $allowExternal = true, ?callable $isWhitelisted = null): static { $this->isUrl = false; - if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) { + if (Preg::isMatch('~^data:image/[a-z]+;base64,~', $path)) { $this->path = $path; return $this; @@ -113,8 +114,12 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip } } // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 - } elseif (filter_var($path, FILTER_VALIDATE_URL) || (preg_match('/^([\w\s\x00-\x1f]+):/u', $path) && !preg_match('/^([\w]+):/u', $path))) { - if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) { + } elseif ( + filter_var($path, FILTER_VALIDATE_URL) + || Preg::isMatch('~^phar://~i', $path) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $path) && !Preg::isMatch('/^([\w.]+):/', $path)) + ) { + if (!Preg::isMatch('/^(http|https|file|ftp|s3):/', $path)) { throw new PhpSpreadsheetException('Invalid protocol for linked drawing'); } if (!$allowExternal) {
tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php+4 −0 modified@@ -161,7 +161,11 @@ public static function providerBadProtocol(): array 'mailto' => ['mailto:xyz@example.com'], 'mailto whitespace' => ['mail to:xyz@example.com'], 'phar' => ['phar://example.com/image.phar'], + 'phar with 3 slashes' => ['phar:///example.com/image.phar'], 'phar control' => ["\x14phar://example.com/image.phar"], + 'filter with phar' => ['php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar'], + 'protocol with period followed by phar' => ['compress.zlib://phar:///x.phar'], + 'protocol with period and embedded space' => ['comp ress.zlib://anything'], ]; } }
tests/PhpSpreadsheetTests/Reader/NoPharTest.php+20 −3 modified@@ -18,10 +18,27 @@ class NoPharTest extends TestCase */ public function testNoPhar(string $reader): void { - $this->expectException(SpreadsheetException::class); - $this->expectExceptionMessage('Stream wrappers are not permitted'); + $invalidProtocol = [ + 'normal phar' => 'phar://anyoldname', + '3 slashes' => 'phar:///anyoldname', + 'mixed case' => 'Phar:///anyoldname', + 'embedded space' => 'ph ar://anyoldname', + 'leading space' => ' phar://anyoldname', + 'embedded control character' => "ph\x04ar://anyoldname", + 'filter with phar' => 'php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar', + 'filter with phar and newline' => "php://filter/read=convert.base64-encode/\nresource=phar:///tmp/x.Phar", + 'protocol with period followed by phar' => 'compress.bzip2://phar:///x.phar', + 'protocol with period and embedded space' => 'comp ress.zlib://anything', + ]; $reader = new $reader(); - $reader->load('phar://anyoldname'); + foreach ($invalidProtocol as $key => $value) { + try { + $reader->load($value); + self::fail("Should have thrown exception - $key"); + } catch (SpreadsheetException $e) { + self::assertStringContainsString('Disallowed stream wrapper', $e->getMessage(), $key); + } + } } /**
1433b34843afSecurity Patch
6 files changed · +210 −103
CHANGELOG.md+10 −0 modified@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). This is always true of the master branch. Some earlier branches, including the branch from which you are reading this file, remain supported and security fixes are applied to them; if the security fix represents a breaking change, it may have to be applied as a minor or patch version. +## 2026-05-30 - 1.30.5 + +### Security Note + +- File::prohibitWrappers and Drawing::setPath now reject phar paths with extra leading slashes (e.g. phar:///…) that escaped the prior parse_url-based filter. + +### Fixed + +- Third-party security patches. + ## 2026-04-19 - 1.30.4 ### Fixed
composer.lock+156 −89 modified@@ -1617,16 +1617,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -1665,28 +1665,28 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -1705,7 +1705,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -1729,9 +1729,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "paragonie/random_compat", @@ -2394,16 +2394,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { @@ -2414,7 +2414,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -2425,11 +2425,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.10", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -2477,7 +2477,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -2488,12 +2488,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "psr/container", @@ -3407,16 +3415,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -3469,15 +3477,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -3667,16 +3687,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -3732,28 +3752,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -3796,15 +3828,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -3977,16 +4021,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -4028,15 +4072,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -4203,27 +4259,27 @@ }, { "name": "setasign/fpdi", - "version": "v2.6.4", + "version": "v2.6.7", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "4b53852fde2734ec6a07e458a085db627c60eada" + "reference": "388c51e69982a3fc16698710b763e8107a49f510" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada", - "reference": "4b53852fde2734ec6a07e458a085db627c60eada", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/388c51e69982a3fc16698710b763e8107a49f510", + "reference": "388c51e69982a3fc16698710b763e8107a49f510", "shasum": "" }, "require": { "ext-zlib": "*", - "php": "^7.1 || ^8.0" + "php": ">=7.2 <=8.5.99999" }, "conflict": { "setasign/tfpdf": "<1.31" }, "require-dev": { - "phpunit/phpunit": "^7", + "phpunit/phpunit": "^8.5.52", "setasign/fpdf": "~1.8.6", "setasign/tfpdf": "~1.33", "squizlabs/php_codesniffer": "^3.5", @@ -4263,15 +4319,15 @@ ], "support": { "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.6.4" + "source": "https://github.com/Setasign/FPDI/tree/v2.6.7" }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", "type": "tidelift" } ], - "time": "2025-08-05T09:57:14+00:00" + "time": "2026-05-13T10:16:22+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -4454,29 +4510,29 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.3", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" } }, "autoload": { @@ -4501,7 +4557,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -4512,12 +4568,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2023-01-24T14:02:46+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/event-dispatcher", @@ -4884,16 +4944,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -4908,8 +4968,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -4943,7 +5003,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -4954,12 +5014,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -5354,21 +5418,20 @@ }, { "name": "symfony/process", - "version": "v5.4.46", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4" + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/01906871cb9b5e3cf872863b91aba4ec9767daf4", - "reference": "01906871cb9b5e3cf872863b91aba4ec9767daf4", + "url": "https://api.github.com/repos/symfony/process/zipball/d9593c9efa40499eb078b81144de42cbc28a31f0", + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -5396,7 +5459,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.46" + "source": "https://github.com/symfony/process/tree/v7.4.11" }, "funding": [ { @@ -5407,12 +5470,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-06T09:18:28+00:00" + "time": "2026-05-11T16:55:21+00:00" }, { "name": "symfony/service-contracts", @@ -5720,16 +5787,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -5758,15 +5825,15 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { "url": "https://github.com/theseer", "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [],
src/PhpSpreadsheet/Shared/File.php+10 −6 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use ZipArchive; @@ -141,19 +142,22 @@ public static function temporaryFilename(): string } /** - * All filenames starting with protocol (e.g. phar://) are prohibited. + * Blocks phar:// and similar RCE-bearing wrappers. * 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. + * data: is intentionally allowed; callers needing strict + * on-disk-only semantics must validate $filename themselves. */ 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) { + if ( + Preg::IsMatch('~^phar://~i', $filename) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $filename) && !Preg::isMatch('/^([\w.]+):/', $filename)) + || Preg::isMatch('~^[\w.]+://.*phar:~is', $filename) + ) { throw new Exception( - "Stream wrappers are not permitted as file paths: {$filename}" + "Disallowed stream wrapper: {$filename}" ); } }
src/PhpSpreadsheet/Worksheet/Drawing.php+10 −5 modified@@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use ZipArchive; @@ -109,7 +110,7 @@ public function getPath() public function setPath($path, $verifyFile = true, $zip = null, $allowExternal = true, ?callable $isWhitelisted = null) { $this->isUrl = false; - if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) { + if (Preg::isMatch('~^data:image/[a-z]+;base64,~', $path)) { $this->path = $path; return $this; @@ -126,8 +127,12 @@ public function setPath($path, $verifyFile = true, $zip = null, $allowExternal = } } // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 - } elseif (filter_var($path, FILTER_VALIDATE_URL) || (preg_match('/^([\w\s\x00-\x1f]+):/u', $path) && !preg_match('/^([\w]+):/u', $path))) { - if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) { + } elseif ( + filter_var($path, FILTER_VALIDATE_URL) + || Preg::isMatch('~^phar://~i', $path) + || (Preg::isMatch('/^([\w.\s\x00-\x1f]+):/', $path) && !Preg::isMatch('/^([\w.]+):/', $path)) + ) { + if (!Preg::isMatch('/^(http|https|file|ftp|s3):/', $path)) { throw new PhpSpreadsheetException('Invalid protocol for linked drawing'); } if (!$allowExternal) { @@ -141,7 +146,7 @@ public function setPath($path, $verifyFile = true, $zip = null, $allowExternal = $ctx = null; // https://github.com/php/php-src/issues/16023 // https://github.com/php/php-src/issues/17121 - if (preg_match('/^https?:/', $path) === 1) { + if (Preg::isMatch('/^https?:/', $path)) { $ctxArray = [ 'http' => [ 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', @@ -151,7 +156,7 @@ public function setPath($path, $verifyFile = true, $zip = null, $allowExternal = ], ], ]; - if (preg_match('/^https:/', $path) === 1) { + if (Preg::isMatch('/^https:/', $path)) { $ctxArray['ssl'] = ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT]; } $ctx = stream_context_create($ctxArray);
tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php+4 −0 modified@@ -161,7 +161,11 @@ public static function providerBadProtocol(): array 'mailto' => ['mailto:xyz@example.com'], 'mailto whitespace' => ['mail to:xyz@example.com'], 'phar' => ['phar://example.com/image.phar'], + 'phar with 3 slashes' => ['phar:///example.com/image.phar'], 'phar control' => ["\x14phar://example.com/image.phar"], + 'filter with phar' => ['php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar'], + 'protocol with period followed by phar' => ['compress.zlib://phar:///x.phar'], + 'protocol with period and embedded space' => ['comp ress.zlib://anything'], ]; } }
tests/PhpSpreadsheetTests/Reader/NoPharTest.php+20 −3 modified@@ -18,10 +18,27 @@ class NoPharTest extends TestCase */ public function testNoPhar(string $reader): void { - $this->expectException(SpreadsheetException::class); - $this->expectExceptionMessage('Stream wrappers are not permitted'); + $invalidProtocol = [ + 'normal phar' => 'phar://anyoldname', + '3 slashes' => 'phar:///anyoldname', + 'mixed case' => 'Phar:///anyoldname', + 'embedded space' => 'ph ar://anyoldname', + 'leading space' => ' phar://anyoldname', + 'embedded control character' => "ph\x04ar://anyoldname", + 'filter with phar' => 'php://filter/read=convert.base64-encode/resource=phar:///tmp/x.Phar', + 'filter with phar and newline' => "php://filter/read=convert.base64-encode/\nresource=phar:///tmp/x.Phar", + 'protocol with period followed by phar' => 'compress.bzip2://phar:///x.phar', + 'protocol with period and embedded space' => 'comp ress.zlib://anything', + ]; $reader = new $reader(); - $reader->load('phar://anyoldname'); + foreach ($invalidProtocol as $key => $value) { + try { + $reader->load($value); + self::fail("Should have thrown exception - $key"); + } catch (SpreadsheetException $e) { + self::assertStringContainsString('Disallowed stream wrapper', $e->getMessage(), $key); + } + } } /**
Vulnerability mechanics
Root cause
"The `parse_url` function incorrectly handles phar URIs with multiple leading slashes, bypassing security checks."
Attack vector
An attacker can provide a specially crafted file path to `IOFactory::load()` that uses the `phar://` stream wrapper with three or more slashes after the scheme (e.g., `phar:///path/to/exploit.phar/file`). This bypasses the `File::prohibitWrappers` check [ref_id=2]. On PHP 7.x, this directly leads to Remote Code Execution via deserialization of PHAR metadata. On PHP 8.x, it results in a file read primitive, which can lead to RCE if the downstream consumer later calls `Phar::getMetadata` [ref_id=2].
Affected code
The vulnerability exists in the `PhpOffice
PhpSpreadsheet
Shared
File::prohibitWrappers` function, which was intended to prevent the use of stream wrappers. The issue also affects `PhpOffice
PhpSpreadsheet
Worksheet
Drawing::setPath` due to similar logic [patch_id=5276383].
What the fix does
The patch modifies the `File::prohibitWrappers` function to use regular expressions via `Composer
Pcre
Preg::isMatch` to more reliably detect and reject disallowed stream wrappers, including the `phar://` scheme with extra slashes [patch_id=5276383]. This prevents the bypass that occurred due to the incorrect parsing of malformed URIs by `parse_url` [ref_id=3]. The `Drawing::setPath` function was also updated to use the same improved logic [patch_id=5276383].
Preconditions
- inputThe application must accept a file path as input to `IOFactory::load()` or a similar function that utilizes `File::prohibitWrappers`.
- inputThe input file path must be a specially crafted URI that bypasses the `parse_url`-based wrapper check, such as `phar:///path/to/file`.
Reproduction
Requires Docker only, no local PHP install. Run:
```sh bash run.sh ```
The script does the following in order: build `exploit.phar` using `php:7.4-cli` with `phar.readonly=0`, install `phpoffice/phpspreadsheet:5.7.0` through composer and run `exploit.php` on `php:8.3-cli` to show that the bypass still works against the latest tag, then install `1.30.4` and run again on `php:7.4-cli` to show the full RCE chain. All output is teed to `evidence.txt`.
`exploit.php` ships two controls. The negative control uses `phar://x/dummy.csv` to confirm that the patch still rejects the standard wrapper form. The positive control uses `phar:///work/exploit.phar/dummy.csv` to show that the three slash variant slips through. On PHP 7.4 the gadget writes the file `pwned_marker` containing the lines `WAKEUP: phpspreadsheet-bypass` and `DESTRUCT: phpspreadsheet-bypass`, which is the proof that attacker controlled code ran inside the victim process. [ref_id=2]
Generated on Jun 8, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.