skip to content

In PHP, how do escapeshellarg() and escapeshellcmd() differ, and which one should wrap a user-supplied filename passed to exec()?

level: middleimportance: must knowfreq 48%

answer

  1. one argument versus a whole command
  2. single quotes around the value
  3. escapeshellcmd leaves spaces alone
  4. paired quotes pass through
  5. a leading dash is still an option

basics

~20 s

escapeshellarg() wraps one value in single quotes so the shell sees exactly one literal argument; use it on every dynamic argument. escapeshellcmd() backslash-escapes metacharacters in a whole command but leaves spaces and paired quotes, so injected extra arguments survive.

solid answer

~50 s

`escapeshellarg($value)` is for **one argument**. On POSIX systems it wraps the value in single quotes and turns each embedded `'` into `'\''`, so the shell passes it as a single literal word: no expansion, no splitting, no command separators. That is what a user-supplied filename needs. `escapeshellcmd($command)` works on a **whole command string**. It backslash-escapes characters such as `;`, `|`, `&`, `$`, backticks and `>`, but not spaces, and quotes only when unpaired. So `report.pdf --output /var/www/x.php` passes through as extra arguments: it blocks chaining a second command, not **argument injection**. Two further traps: neither stops a value starting with `-` from being read as an option, so I use `--` or validate; and running `escapeshellcmd()` over an already `escapeshellarg()`-quoted string can break the quoting. Better still, `proc_open()` with an array needs no escaping at all.

code

php · 10 lines
php
<?php
declare(strict_types=1);

$pdf = "it's; rm -rf ~.pdf"; // hostile upload name

echo escapeshellarg($pdf), "\n";
// 'it'\''s; rm -rf ~.pdf'   (one literal word on POSIX)

echo escapeshellcmd('pdfthumb ' . $pdf), "\n";
// pdfthumb it\'s\; rm -rf \~.pdf   (spaces untouched: still several words)

go deeper

for a junior

Remember: escapeshellarg() for each dynamic argument, and never splice raw input into a command string.

for a middle

Explain how each function works on POSIX, why escapeshellcmd() allows argument injection through spaces, and why a leading dash still matters.

for a senior

Review call sites for escapeshellcmd() misuse and double escaping, prefer generated filenames and --, and move to proc_open() arrays to remove the shell entirely.

for a principal

Set a codebase rule that shell strings are built only through one audited helper or replaced by array commands, and enforce it with static analysis.

## Two functions with different jobs When PHP runs a command through `exec()`, `system()`, `passthru()` or `shell_exec()`, the string is parsed by a **shell**. The shell splits it into words at spaces, expands `$VARS` and globs, and treats characters such as `;`, `|` and `&` as instructions. Anything dynamic that you splice into that string must be neutralised first. PHP offers two functions, and they are not interchangeable. | | `escapeshellarg(string $arg): string` | `escapeshellcmd(string $command): string` | |---|---|---| | Input | one argument value | a complete command line | | POSIX technique | wrap in `'...'`; each `'` becomes `'\''` | prefix metacharacters with a backslash | | Characters handled | everything, since single quotes disable all expansion | shell metacharacters such as `;`, `&`, `$`, `>`, `*`, the backtick and the pipe, plus `\x0A` and `\xFF`; `'` and `"` only if unpaired | | Spaces | kept inside the one quoted word | **not escaped**, so they still split arguments | | Protects against | command injection and argument splitting | chaining extra commands only | ## Why escapeshellarg() is the right tool For the thumbnail feature, the uploaded PDF's path and the output path are arguments: ```php $cmd = 'pdfthumb --width 320 ' . escapeshellarg($pdf) . ' ' . escapeshellarg($png); ``` Whatever `$pdf` contains, including spaces, quotes, `$(...)` or `;`, the shell receives one word with exactly that content. On Linux, `escapeshellarg("it's.pdf")` returns `'it'\''s.pdf'`: close the quote, add an escaped quote, reopen. ## Why escapeshellcmd() is not enough Suppose the code builds the whole line first and then escapes it: ```php $cmd = escapeshellcmd('pdfthumb --width 320 ' . $pdf); ``` With `$pdf = 'a.pdf --output /var/www/public/x.php'`, nothing gets escaped, because the value holds no metacharacters. The spaces still split words, so the tool receives two extra arguments chosen by the attacker. Depending on the tool, options like that can write files, read files or load configuration. This is **argument injection**, and `escapeshellcmd()` does nothing about it. It also leaves **paired** quotes alone, so quoted chunks can be smuggled in as well. Combining the two is also a trap. Applying `escapeshellcmd()` to a string that already contains `escapeshellarg()` output re-escapes characters inside the single-quoted part and can break the quoting it relied on. Escape each argument with `escapeshellarg()`, concatenate, and stop. ## The leading dash problem Neither function changes how the *program* interprets its arguments. `escapeshellarg('--help')` is `'--help'`, and after the shell removes the quotes, the tool still sees an option. For filenames: 1. Put `--` before positional arguments when the tool follows the common end-of-options convention. 2. Or make paths absolute (or prefix `./`), so they cannot begin with `-`. 3. Better, never use the user's filename at all: store uploads under a generated name, and pass that. ## Other caveats - **Locale.** Both functions depend on the `LC_CTYPE` locale for multibyte strings; the manual notes that unrecognised characters are discarded. - **Windows.** The rules differ: `escapeshellarg()` uses double quotes and replaces `%`, `!` and `"` with spaces, and `escapeshellcmd()` uses the caret. Code that must be portable needs testing on both. - **Length.** `escapeshellarg()` throws `ValueError` when the argument exceeds the platform's maximum command length. ## A review checklist - Every dynamic value in a command string passes through `escapeshellarg()` individually, at the point of concatenation. - No `escapeshellcmd()` on strings that contain user input, and never on top of `escapeshellarg()` output. - Positional arguments from users are preceded by `--` or are generated names. - Paths are absolute, and the tool itself is called by absolute path. - The exit status is checked, and error output is captured with `2>&1` or a separate pipe. ## Avoiding escaping altogether Since PHP 7.4, `proc_open()` accepts the command as an **array**, such as `['pdfthumb', '--width', '320', '--', $pdf, $png]`. PHP then starts the program directly, without a shell, and each element becomes exactly one argument. There is nothing to escape, though the leading-dash rule still applies, because the program itself still parses options.

  • Is a filename wrapped in escapeshellarg() always safe to pass to a command-line tool?
    Safe from the shell, not necessarily from the tool. If the value starts with `-`, the program still parses it as an option after the shell strips the quotes. Put `--` before positional arguments when the tool supports it, use absolute paths, or pass a generated filename instead of the user's.
  • Why is `escapeshellcmd(escapeshellarg($x))` a bad idea?
    `escapeshellcmd()` treats its input as a whole command and backslash-escapes characters that sit inside `escapeshellarg()`'s single quotes, where the shell does not process backslashes. The quoting that made the value one literal word can be broken or altered. Escape each argument once with `escapeshellarg()` and do not post-process the command.
  • What does escapeshellarg("it's") return on Linux?
    `'it'\''s'`. The function wraps the value in single quotes, and for each embedded single quote it closes the quoted section, adds a backslash-escaped quote, and reopens. The shell rejoins the pieces into the single word `it's`.

saying these in an interview costs you the question

  • escapeshellcmd() is enough to make a user-supplied filename safe.
  • escapeshellarg() stops a value like --output=x from being read as an option.
  • Applying both escapeshellarg() and escapeshellcmd() is the most secure option.
  • escapeshellcmd() escapes spaces so extra arguments cannot be injected.
  • An array command in proc_open() still needs escapeshellarg() on each element.