Pierre-Henry Soria – CTO Insights, Software Architecture & Product Leadership

Run an External Program from PHP Without a Fragile Shell Command

My original article used PHP’s exec() function to start a Perl script and print its output. The example explained the function’s output and return-code arguments, but it did not define a security boundary or handle failures.

Starting another program is sometimes the right solution. Building a command string from request data is not.

I First Check Whether I Need a Process

I prefer a library or service API when one exists. It gives me typed inputs, structured errors, and fewer operating-system differences.

An external process still makes sense when I need a mature command-line tool, an existing worker, or a program written in another language. In that case, I treat the executable as part of my application’s trusted configuration. A visitor never chooses which binary is run.

I Pass Arguments as an Array

For a Composer project, the Symfony Process component provides a clear interface around process execution:

1composer require symfony/process

Here is a small example with an explicit allowlist:

 1<?php
 2
 3use Symfony\Component\Process\Exception\ProcessFailedException;
 4use Symfony\Component\Process\Process;
 5
 6$mode = $_POST['mode'] ?? '';
 7$allowedModes = ['preview', 'export'];
 8
 9if (!in_array($mode, $allowedModes, true)) {
10    throw new InvalidArgumentException('Unsupported mode.');
11}
12
13$phpBinary = '/usr/bin/php'; // Fixed in deployment configuration.
14
15$process = new Process([
16    $phpBinary,
17    __DIR__ . '/worker.php',
18    '--mode',
19    $mode,
20]);
21$process->setTimeout(30);
22
23try {
24    $process->mustRun();
25    $result = trim($process->getOutput());
26} catch (ProcessFailedException $exception) {
27    // Record an internal failure without exposing command details to the user.
28    throw new RuntimeException('The worker failed.', 0, $exception);
29}

The executable and every argument are separate array elements. I keep the CLI path in deployment configuration and verify it when the application starts. I do not concatenate $mode into a shell command. Symfony recommends this form because it handles argument escaping and avoids invoking shell features that are not needed.

Native PHP Can Also Avoid a Command String

When adding a dependency is not justified, proc_open() accepts the command as an array on PHP 7.4 and later. PHP can then start the process directly and manage its arguments without passing a command string through a shell.

proc_open() also exposes standard input, standard output, and standard error through descriptors. That control is useful, but it creates more code to close pipes, collect output, and release the process correctly. I use it when I need that control and test every exit path.

The older exec() function still returns the last output line and can populate an output array and result code. It accepts only a command string, so any variable argument requires careful shell handling. I avoid it when an argument-array API can do the job.

I Define the Operating Limits

The code that starts a process is only one part of the design. I also define:

I never run the web application as root. I also avoid starting a long task inside an HTTP request. Work that can outlive the request belongs in a queue with retry rules, duplicate protection, and a visible result.

I Test Failure, Not Only Output

My original example proved that PHP could print the lines returned by a Perl script. A production test must also cover an unknown mode, a missing executable, a non-zero exit code, a timeout, large output, and a process that writes only to standard error.

The useful idea remains the same: PHP can coordinate an existing program. The current version adds the boundary that the first article lacked. The application chooses the program, validates each argument, limits execution, and treats every failure as data to handle.


Pierre-Henry Soria

GitHub · PierreHenry.Dev · YouTube

<< Previous Post

|

Next Post >>

#PHP #Symfony Process #Security #Subprocesses #Backend Development #Reliability