note 123718 modified in function.escapeshellarg by daverandom
| From: | daverandom@php.net | Date: | Sun, 24 Mar 2019 02:21:52 +0000 |
| Subject: | note 123718 modified in function.escapeshellarg by daverandom | ||
| References: | 1 | Groups: | php.notes |
| Request: | Send a blank email to php-notes+get-210432@lists.php.net to get a copy of this message | ||
On Windows, this function naively strips special characters and replaces them with spaces. The
resulting string is always safe for use with exec() etc, but the operation is not lossless - strings
containing " or % will not be passed through to the child process correctly.
Correctly escaping shell commands on Windows is not a simple matter. Programs must consider two
distinct escape mechanisms which serve different purposes:
1) The convention used by the CommandLineToArgV() windows system function, used by the child
process to interpret the command line string
2) The convention used by cmd.exe to escape shell meta-characters (e.g. output redirection
controls)
All commands should be escaped for CommandLineToArgV() - this mechanism is applied to each argument
individually before it is appended to the command line string. The resulting string may be safely
used with the CreateProcess() family of system functions. However...
In almost all cases when creating a child process from PHP on Windows, it is done indirectly by
invoking cmd.exe - this is to enable the use of shell functionality such as I/O redirection and
environment variable substitution. As a consequence, the entire command string must be further
escaped for cmd.exe. If the executed command contains further indirect calls through cmd.exe, each
child command must be escaped again for each level of indirection.
The following functions can be used to correctly escape strings such that they are safely passed
through to a child process:
<?php
/**
* Escape a single value in accordance with CommandLineToArgV()
* https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85)
*/
function escape_win32_argv(string $value): string
{
static $expr = '(
[\x00-\x20\x7F"] # control chars, whitespace or double quote
| \\\\++ (?=("|$)) # backslashes followed by a quote or at the end
)ux';
if ($value === '') {
return '""';
}
$quote = false;
$replacer = function($match) use($value, &$quote) {
switch ($match[0][0]) { // only inspect the first byte of the match
case '"': // double quotes are escaped and must be quoted
$match[0] = '\\"';
case ' ': case "\t": // spaces and tabs are ok but must be quoted
$quote = true;
return $match[0];
case '\\': // matching backslashes are escaped if quoted
return $match[0] . $match[0];
default: throw new InvalidArgumentException(sprintf(
"Invalid byte at offset %d: 0x%02X",
strpos($value, $match[0]), ord($match[0])
));
}
};
$escaped = preg_replace_callback($expr, $replacer, (string)$value);
if ($escaped === null) {
throw preg_last_error() === PREG_BAD_UTF8_ERROR
? new InvalidArgumentException("Invalid UTF-8 string")
: new Error("PCRE error: " . preg_last_error());
}
return $quote // only quote when needed
? '"' . $escaped . '"'
: $value;
}
/** Escape cmd.exe metacharacters with ^ */
function escape_win32_cmd(string $value): string
{
return preg_replace('([()%!^"<>&|])', '^$0', $value);
}
/** Like shell_exec() but bypass cmd.exe */
function noshell_exec(string $command): string
{
static $descriptors = [['pipe', 'r'],['pipe',
'w'],['pipe', 'w']],
$options = ['bypass_shell' => true];
if (!$proc = proc_open($command, $descriptors, $pipes, null, null, $options)) {
throw new \Error('Creating child process failed');
}
fclose($pipes[0]);
$result = stream_get_contents($pipes[1]);
fclose($pipes[1]);
stream_get_contents($pipes[2]);
fclose($pipes[2]);
proc_close($proc);
return $result;
}
// usage
$badString = 'String with "C:\\quotes\\" or malicious %OS% stuff \\';
$cmdParts = [
'php',
'-d', 'display_errors=1', '-d', 'error_reporting=-1',
'-r', 'echo $argv[1];',
$badString // child process $argv[1] value
];
/* The typical approach - works fine on POSIX shells but totally wrong
on Windows */
$wrong = implode(' ', array_map('escapeshellarg', $cmdParts));
/* Always escape each argument individually */
$escaped = implode(' ', array_map('escape_win32_argv', $cmdParts));
/* In almost all cases, escape for cmd.exe as well - the only exception is
when using proc_open() with the bypass_shell option. cmd doesn't handle
arguments individually, so the entire command line string can be escaped,
no need to process arguments individually */
$cmd = escape_win32_cmd($escaped);
$cmds = [
'escapeshellarg() - wrong' => $wrong,
'escape_win32_argv() - correct for bypass_shell' => $escaped,
'escape_win32_cmd(escape_win32_argv()) - correct everywhere else' => $cmd,
];
function check($original, $received)
{
$match = $original === $received ? '=' : 'X';
return "$match '$received'";
}
foreach ($cmds as $description => $cmd) {
echo "$description\n";
echo " $cmd\n";
echo " original: '$badString'\n";
echo " shell_exec(): " . check($badString, shell_exec($cmd)) . "\n";
echo " noshell_exec(): " . check($badString, noshell_exec($cmd)) . "\n";
echo "\n";
}
--was--
On Windows, this function naively strips special characters and replaces them with spaces. The
resulting string is always safe for use with exec() etc, but the operation is not lossless - strings
containing " or % will not be passed through to the child process correctly.
Escaping shell commands on Windows is not a simple matter. There are two distinct escape mechanisms
which serve different purposes:
1) The convention used by the CommandLineToArgV() windows system function, used by the child
process to interpret the command line arguments
2) The convention used by cmd.exe to escape meta-characters
In almost all cases when creating a child process from PHP on Windows, it is done by invoking
cmd.exe - this is to enable the use of shell functionality such as I/O redirection and environment
variable substitution. As a consequence, values must be escaped first for CommandLineToArgV(), then
for cmd.exe. If the executed command contains further indirect calls through cmd.exe, values must be
escaped again for each level of indirection.
The following functions can be used to correctly escape strings such that they are safely passed
through to a child process:
<?php
/**
* Escape a single value in accordance with CommandLineToArgV()
* https://docs.microsoft.com/en-us/previous-versions/17w5ykft(v=vs.85)
*
* @param string $value
* @return string
*/
function escape_win32_argv($value)
{
$expr = '(
[\x00-\x20\x7F"] # control chars, whitespace or double quote
| \\\\++ (?=("|$)) # backslashes followed by a quote or at the end
)ux';
$quote = false;
$replacer = function($match) use($value, &$quote) {
switch ($match[0][0]) { // only inspect the first byte of the match
case '"': // double quotes are escaped and must be quoted
$match[0] = '\\"';
case ' ': case "\t": // spaces and tabs are ok but must be quoted
$quote = true;
return $match[0];
case '\\': // matching backslashes are escaped if quoted
return $match[0] . $match[0];
default: throw new InvalidArgumentException(sprintf(
"Invalid byte at offset %d: 0x%02X",
strpos($value, $match[0]), ord($match[0])
));
}
};
$escaped = preg_replace_callback($expr, $replacer, (string)$value);
if ($escaped === null) {
throw preg_last_error() === PREG_BAD_UTF8_ERROR
? new InvalidArgumentException("Invalid UTF-8 string")
: new Error("PCRE error: " . preg_last_error());
}
return $quote // only quote when needed
? '"' . $escaped . '"'
: $value;
}
/**
* Escape cmd.exe metacharacters with ^
*
* @param string $value
* @return string
*/
function escape_win32_cmd($value)
{
return preg_replace('([()%!^"<>&|])', '^$0', (string)$value);
}
// usage
$badString = 'String with "C:\\quotes\\" or malicious %OS% stuff \\';
$cmdParts = ['php', '-r', 'echo $argv[1];', $badString];
// Always escape each argument individually
$escaped = implode(' ', array_map('escape_win32_argv', $cmdParts));
// In most cases, escape for cmd.exe as well
// The only exception is when using proc_open() with the bypass_shell option
$cmd = escape_win32_cmd($escaped);
$result = shell_exec($cmd);
echo <<<STR
Escaped: $escaped
Cmd: $cmd
Original: '$badString'
Received: '$result'
STR;
http://php.net/manual/en/function.escapeshellarg.php