CVE-2026-49260 is a high-severity OS command injection vulnerability in pontedilana/php-weasyprint (composer), affecting versions <= 2.5.0. It is fixed in 2.5.1.
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 isexecutable(). On POSIX escapeshellarg('/usr/local/bin/weasyprint') returns '/usr/local/bin/weasyprint' with the single-quote characters as part of the string, so isexecutable() 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 <= 2.5.0 (current master tip commit c2b51fed0bf442c3bf0292b879a09944d436f2a0, 2026-04-03). Patched in: 2.5.1 Privilege required Any caller that can influence the binary string handed to the Pdf constructor or to AbstractGenerator::setBinary(). Typical reach paths: An application config file (config/services.yaml, .env, helm chart value) read at boot time, where the path is auto-detected from environment or driven by a per-tenant override. An admin UI that lets operators pick between multiple WeasyPrint builds (weasyprint-v60, weasyprint-v66) for compatibility reasons. A multi-tenant SaaS that resolves binary location from a tenant config row. Once an attacker plants a string containing shell metacharacters in one of those channels, every subsequent generate() call shells out the injected payload as the PHP process user. Vulnerable code src/AbstractGenerator.php#L169-L172: src/Pdf.php#L167-L170 overrides buildCommand with the same guard: escapeshellarg($binary) returns a single-quoted string. isexecutable() then looks up a file whose name literally contains the surrounding single-quote characters, which essentially never exists. The ternary therefore always falls through to the right-hand side, where $command is the raw, unescaped $binary string. The rest of the command construction (options, input, output) is correctly escaped, so injection has to land in the binary segment, which is exactly the segment configuration-driven deployments treat as trusted. This is the same primitive KnpLabs/snappy patched in version 1.7.1. The README of php-weasyprint states: "This library is massively inspired by KnpLabs/snappy, of which it aims to be a one-to-one substitute (GeneratorInterface is the same)." The vulnerable buildCommand was copied verbatim and never updated. How $binary reaches the shell No intermediate validator, no scheme check, no allow-list. Whatever string reaches setBinary() is shell-evaluated. Proof of concept The # at the end of $binaryString comments out the unrelated '/dev/null' '/tmp/pocout.pdf' tail that buildCommand appends, keeping the shell line syntactically valid. End-to-end reproduction (against pinned Composer install) Captured run output (PHP 8.5.6, macOS arm64): 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/phpweasyprintrcemarker 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: Even the benign path is emitted raw (without single-quotes around the binary), confirming the isexecutable() 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 (!\isexecutable($binary)) throw new RuntimeException(...); $command = \escapeshellarg($binary);) and re-running the same harness: The corrected guard runs isexecutable() on the unescaped $binary. For the attacker payload that lookup returns false (no file by that name exists on disk), the exception fires before Process::fromShellCommandline is ever called, and the marker file is never created. Impact Shell-command injection as the PHP-FPM / CLI user whenever the WeasyPrint binary path is influenced by configuration, environment, or per-tenant settings. Affects every consumer that does not hard-code a constant binary path baked into the deployed code. Empirically, both the project's own README and tests demonstrate the binary path as a configurable constructor argument (new Pdf('/usr/local/bin/weasyprint')), and downstream framework integrations (Symfony / Laravel) typically wire it through container config. Defensive-in-depth regression even for hard-coded paths: a reader of buildCommand reasonably expects the binary to be shell-escaped because the code visually claims to do so. Any later change that reads the binary from a less-trusted source inherits the dead guard. CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H (7.6, High), adjust to AV:N when the binary path is reachable from an unauthenticated request surface (e.g. an admin endpoint without proper auth). Suggested fix Mirror the KnpLabs/snappy 1.7.1 fix shape exactly (the upstream library this project explicitly mirrors): Apply the identical change to src/Pdf.php::buildCommand. The is_executable() check now runs against the raw $binary (the only string that can name a real file on disk), and the escapeshellarg() call only quotes a string that has already been verified as a real executable path on the local filesystem. A regression test that asserts buildCommand throws on a $binary string containing ; / && / | should be added so the dead-guard pattern cannot reappear silently. Credit Reported by tonghuaroot.
Untrusted input reaches a shell command, allowing arbitrary commands to run on the host. Typical impact: code execution in the application's environment.
CVE-2026-49260 has a CVSS score of 8.2 (High). The vector is requires local access, high privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment.
A fixed version is available (2.5.1). Upgrading removes the vulnerable code path.
composer
pontedilana/php-weasyprint (<= 2.5.0)pontedilana/php-weasyprint → 2.5.1 (composer)Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter instead of chasing every advisory.
Kodem's Application Detection and Response identifies whether CVE-2026-49260 is reachable in your applications. Explore runtime application protection for your team.
See if CVE-2026-49260 is reachable in your applications. Get a demo
Already deployed Kodem? See CVE-2026-49260 in your environment →Upgrade pontedilana/php-weasyprint to 2.5.1 or later to resolve this vulnerability.
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
CVE-2026-49260 is a high-severity OS command injection vulnerability in pontedilana/php-weasyprint (composer), affecting versions <= 2.5.0. It is fixed in 2.5.1. Untrusted input reaches a shell command, allowing arbitrary commands to run on the host.
CVE-2026-49260 has a CVSS score of 8.2 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
pontedilana/php-weasyprint (composer) versions <= 2.5.0 is affected.
Yes. CVE-2026-49260 is fixed in 2.5.1. Upgrade to this version or later.
Whether CVE-2026-49260 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
Upgrade pontedilana/php-weasyprint to 2.5.1 or later.