# Evitar la inyección de comandos (command injection) en PHP

> Evitar la inyección de comandos en PHP: prescindir de la shell, proc_open con array, usar bien escapeshellarg y encontrar llamadas peligrosas en el código.

Fuente: https://phpsos.de/es/blog/command-injection-php/  
Actualizado: 25/10/2026  
Proveedor: PHP SOS, Ayuda de emergencia para PHP y seguridad de servidores. Ayuda inmediata por WhatsApp: https://wa.me/491736494914?text=Hola%20PHP%20SOS%2C%20necesitamos%20ayuda%20con%20nuestro%20servidor%20PHP.%20Empresa%3A%20%E2%80%A6%20Sitio%20web%3A%20%E2%80%A6

**En resumen:** la forma más segura de evitar la inyección de comandos en PHP es no invocar ninguna shell: muchas tareas las resuelven directamente funciones o bibliotecas de PHP. Donde se necesite un programa externo, pase el comando como array a `proc_open()` (desde PHP 7.4); entonces PHP inicia el programa sin shell. Compruebe los valores frente a una lista de permitidos, cierre las opciones con `--` y ejecute el proceso con los mínimos permisos.

## Por qué una llamada al sistema da tanto poder

Edición de imágenes, generación de PDF, archivado, análisis antivirus, consulta de un registro DNS: para tareas así, las aplicaciones PHP suelen invocar programas externos. Técnicamente es cómodo; basta una línea con `exec()` o `shell_exec()`. En términos de seguridad, es uno de los puntos más peligrosos del código.

Si un comando se compone como texto y lo ejecuta una shell, es la shell quien decide qué es comando y qué son datos. Si un valor externo llega sin comprobar, puede alterar el significado del comando. En el peor caso, la consecuencia es la misma que con PHP2Shell: ejecución de comandos arbitrarios en el servidor con los permisos del proceso PHP.

La diferencia: PHP2Shell se corrige con una actualización de PHP. Una inyección de comandos en el propio código persiste tras cada actualización. A ello se suma que esos puntos suelen estar en scripts auxiliares pensados solo para administradores, por ejemplo para mantenimiento, diagnóstico o importaciones, y que luego siguen siendo accesibles sin protección.

## Dónde surge la inyección de comandos en PHP

Está en riesgo cualquier punto en el que PHP inicia una shell o un programa. Las funciones para ello son:

| Función | ¿Usa una shell? | Observación |
|---|---|---|
| `exec()`, `system()`, `passthru()`, `shell_exec()` | sí | Comando como texto, la shell lo interpreta |
| Operador de comillas invertidas | sí | Equivale a `shell_exec()` |
| `popen()` | sí | Comando como texto |
| `proc_open()` con cadena | sí | Comando como texto |
| `proc_open()` con array | no | Desde PHP 7.4, los argumentos se pasan directamente |
| `mail()` con quinto parámetro | indirectamente | Parámetros adicionales al programa de correo |

Un patrón típicamente inseguro es este. El nombre de archivo procede de la petición y acaba directamente en la línea de comandos:

```php
<?php
// INSEGURO: un valor de la petición pasa a formar parte de un comando de shell
$datei = $_POST['datei'];
exec('convert uploads/' . $datei . ' -resize 800x800 vorschau/' . $datei);
```

## Primera opción: ninguna shell

La protección más eficaz es sustituir la llamada al sistema. OWASP lo cita expresamente como primera línea de defensa. Para muchas tareas, PHP tiene funciones o extensiones propias:

- Directorios y archivos: `mkdir()`, `copy()`, `rename()`, `unlink()`
- Archivos comprimidos: `ZipArchive` o `PharData`
- Imágenes: GD o Imagick como extensión de PHP
- Consultas de red: `dns_get_record()`, `gethostbyname()`, cURL
- Sumas de comprobación: `hash_file()`

Así desaparece por completo la cuestión de qué caracteres interpreta una shell. Además, las funciones de comandos del sistema pueden entonces bloquearse en la configuración, como describe la guía [Usar bien disable_functions](https://phpsos.de/es/ratgeber/disable-functions/).

## Si hace falta un programa externo: proc_open con array

Desde PHP 7.4, `proc_open()` acepta el comando como array. Según el manual de PHP, el proceso se inicia entonces directamente, sin pasar por la shell, y PHP se encarga del escapado necesario de los argumentos. Caracteres como el punto y coma o la barra vertical dejan de tener significado especial.

```php
<?php
declare(strict_types=1);

$name = basename((string) ($_POST['datei'] ?? ''));
if (!preg_match('/^[a-z0-9_-]{1,64}\.(jpg|png)$/', $name)) {
    throw new InvalidArgumentException('Ungültiger Dateiname.');
}

$befehl = ['/usr/bin/convert', '/srv/app/uploads/' . $name, '-resize', '800x800',
           '/srv/app/vorschau/' . $name];

$prozess = proc_open($befehl, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($prozess)) {
    throw new RuntimeException('Programm konnte nicht gestartet werden.');
}
$fehlerausgabe = stream_get_contents($pipes[2]);
$status = proc_close($prozess);
```

Tres cosas hacen robusto el ejemplo: el programa figura en el código con su ruta completa, el nombre de archivo se comprueba frente a un patrón estricto y cada argumento variable empieza por una ruta absoluta, de modo que no puede leerse como opción. Donde un valor aparece solo, cierre las opciones con `--`, siempre que el programa lo admita según su documentación, o rechace los valores que empiecen por un guion.

Para tareas que necesitan programas externos con regularidad, por ejemplo procesamiento de imágenes o PDF a mayor escala, merece la pena desacoplar: la aplicación web solo deja un trabajo en una cola, y un servicio en segundo plano separado, con usuario y permisos propios, lo ejecuta. El proceso PHP que atiende peticiones de internet ya no necesita entonces funciones de comandos del sistema, y pueden bloquearse para él. Los programas externos deberían además tener límites de tiempo y de memoria, para que un trabajo individual no bloquee el servidor.

Un caso especial es `mail()`. El quinto parámetro pasa opciones adicionales al programa de correo del sistema. Los valores de formularios, por ejemplo una dirección de remitente, nunca deben llegar allí sin comprobar. Más robusta es una biblioteca de correo que envíe por SMTP y no necesite ninguna llamada al sistema.

## escapeshellarg: usarlo bien y conocer sus límites

Donde el código existente aún trabaja con líneas de comandos, `escapeshellarg()` para cada valor individual es la medida mínima. La función encierra el valor entre comillas, de modo que la shell lo trate como exactamente un argumento. `escapeshellcmd()` no es adecuado para entradas de usuario porque permite parámetros adicionales, como muestra OWASP con un ejemplo.

Límites importantes de `escapeshellarg()`:

1. **La inyección de argumentos sigue siendo posible:** un valor escapado que empiece por un guion lo lee igualmente el programa de destino como opción.
2. **Windows se comporta de otro modo:** allí, según el manual, la función sustituye ciertos caracteres como `%` y `"` por espacios; los valores llegan, por tanto, alterados.
3. **Juego de caracteres:** según la configuración regional del servidor, pueden perderse caracteres no ASCII.

## Encontrar los puntos afectados en el código existente

Una búsqueda de llamadas al sistema se hace rápido y arroja una lista manejable:

```bash
grep -rnE "\b(exec|system|passthru|shell_exec|popen|proc_open)\s*\(" src/ --include="*.php"
grep -rn '`' src/ --include="*.php" | grep -v '^\s*//'
```

Cada hallazgo necesita una respuesta a tres preguntas: ¿puede sustituirse la llamada por funciones de PHP? ¿Entran valores del exterior? ¿Con qué permisos se ejecuta el proceso? La última pregunta es decisiva para el daño en el peor caso. Un proceso PHP con usuario propio y sin permisos de escritura sobre el código causa menos daño; véase [Mínimo privilegio: los permisos del usuario del servidor web](https://phpsos.de/es/blog/rechte-des-webserver-benutzers/).

> **Importante:**
> Si un proceso de PHP-FPM o del servidor web inicia una shell, es una señal de alarma fuerte de una intrusión. Cómo reconocerlo lo describe [Señal de alarma: el servidor web inicia una shell](https://phpsos.de/es/blog/webserver-startet-shell/).

## ¿Cómo ayuda PHP SOS en concreto?

- Buscamos todas las llamadas al sistema en su aplicación, en los plugins y en los scripts auxiliares.
- Sustituimos las llamadas, donde es posible, por funciones de PHP y pasamos las restantes a llamadas sin shell.
- Bloqueamos en la configuración de PHP las funciones que no se necesitan y revisamos los permisos del usuario de PHP.
- Probamos los cambios y documentamos qué programas puede seguir invocando la aplicación.

La revisión de software propio la ofrecemos como [auditoría de seguridad y prueba de penetración](https://phpsos.de/es/leistungen/sicherheitsaudit-penetrationstest/).


## Preguntas frecuentes

**¿Basta escapeshellarg contra la inyección de comandos?**  
Impide que un valor se ejecute como un comando adicional. No impide que el programa invocado lea el valor como una opción cuando empieza por un guion. Por eso, compruebe además los valores y, donde el programa lo admita, cierre las opciones con --.

**¿Qué diferencia hay entre escapeshellarg y escapeshellcmd?**  
escapeshellarg trata un valor individual como exactamente un argumento. escapeshellcmd solo escapa metacaracteres en una línea de comandos completa y permite parámetros adicionales. Para entradas de usuario, escapeshellarg es la elección correcta, y una llamada sin shell es aún mejor.

**¿Debería bloquear exec y system con disable_functions?**  
Si su aplicación no las necesita, sí. Eso limita el daño tras una intrusión. Pero no sustituye a un código seguro, porque las aplicaciones que necesitan comandos siguen necesitando esas funciones.

**¿Cómo reconozco si se ha explotado una inyección de comandos?**  
Una señal de alarma fuerte son procesos de PHP o del servidor web que arrancan una shell como sh o bash. A ello se suman procesos desconocidos, archivos nuevos en las carpetas de subidas y conexiones salientes inusuales.

## Fuentes

- [OWASP OS Command Injection Defense Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html)
- [Manual de PHP: proc_open](https://www.php.net/manual/de/function.proc-open.php)
- [Manual de PHP: escapeshellarg](https://www.php.net/manual/de/function.escapeshellarg.php)
