Externe Skripte in PHP sicher ausführen
Mit Symfony Process und einem Argument-Array führen Sie ein vertrauenswürdiges externes Skript aus, ohne dessen Argumente in Shell-Befehle zu verwandeln. Diese CLI-Anleitung erstellt ein Projekt, erfasst die Begrüßung eines Kindprozesses und zeigt, wie Sie einen fehlerhaften Exit melden oder einen langsamen Kindprozess stoppen. Der Kindprozess läuft weiterhin mit den Berechtigungen Ihres Kontos. Sichere Argumentverarbeitung ist daher keine Sandbox.
Voraussetzungen
Die folgenden Beispiele wurden unter Linux mit PHP CLI 8.5.10, Composer 2.10.3, Bash 5.3.15 und Symfony Process 7.4.19 reproduziert. Verwenden Sie diese Umgebung, um die Anleitung nachzuvollziehen; andere Laufzeitversionen und das Verhalten unter Windows werden hier nicht behandelt. PHP 8.5 ist ein aktiv unterstützter PHP-Versionszweig.
Sie benötigen php und composer in einem
vertrauenswürdigen PATH, ein beschreibbares übergeordnetes Verzeichnis und
die aktivierte PHP-Funktion proc_open.
Symfony Process verwendet proc_open,
um den Kindprozess zu starten. Composer benötigt außerdem seine üblichen PHP-Erweiterungen und
Netzwerkzugriff, um die Abhängigkeit herunterzuladen.
Das Aufrufskript und das Kindskript verwenden das
PHP-Flag -n,
das php.ini ignoriert. So bleibt das CLI-Beispiel unabhängig von der lokalen
PHP-Konfiguration. Für eine bestehende Anwendung wird diese Konfiguration nicht empfohlen.
Composer läuft dagegen mit seiner normalen PHP-Konfiguration.
Umgebung einrichten
Fügen Sie diesen Block in Bash ein, während Sie sich in dem Verzeichnis befinden, in dem Sie das Projekt erstellen möchten:
(
if [ -n "${COMPOSER:-}" ] || [ -n "${COMPOSER_VENDOR_DIR:-}" ]; then
printf '%s\n' 'Use the default Composer manifest and vendor directory for this example.' >&2
exit 1
fi
mkdir -- php-external-scripts &&
cd -- php-external-scripts &&
export COMPOSER_HOME="$PWD/.composer-home" &&
composer --no-plugins --no-scripts init \
--name=example/php-external-scripts \
--description='PHP child process example' \
--require='symfony/process:7.4.19' \
--no-interaction &&
composer --no-plugins --no-scripts install --no-interaction
)
Die Klammern beschränken Verzeichniswechsel und Umgebungsänderungen auf eine Subshell. Jeder
abhängige Befehl wird mit && verknüpft. Schlägt also
mkdir, cd oder ein Composer-Befehl fehl, wird die
Einrichtung gestoppt, ohne spätere Schritte im falschen Verzeichnis auszuführen. Ihre Shell bleibt
sowohl bei Erfolg als auch bei einem Fehler im übergeordneten Verzeichnis.
Falls php-external-scripts bereits existiert, wählen Sie ein anderes übergeordnetes
Verzeichnis. Löschen Sie kein bestehendes Projekt, um diesen Befehl erfolgreich auszuführen.
Eine fehlgeschlagene Installation kann das neu erstellte Verzeichnis zurücklassen. Prüfen Sie es
daher, bevor Sie entscheiden, ob Sie es an anderer Stelle erneut versuchen.
Das lokale Composer-Home verhindert, dass globale Projekteinstellungen oder Plugins übernommen
werden. Die Schutzprüfung weist Überschreibungen für das Manifest und das Verzeichnis vendor zurück.
Der Composer-Befehl install
erstellt composer.lock und vendor/ in diesem neuen Projekt.
Die genaue Process-Version macht dieses Beispiel reproduzierbar. Prüfen Sie Aktualisierungen der
Abhängigkeiten separat, bevor Sie es in einer Anwendung einsetzen.
Ein eigenes Skript zur Ausführung schreiben
Speichern Sie dies als php-external-scripts/hello.php:
<?php
$name = $argv[1] ?? 'World';
$mode = $argv[2] ?? 'hello';
if ($mode === 'fail') {
fwrite(STDERR, "Example child failed.\n");
exit(23);
}
if ($mode === 'slow') {
sleep(5);
}
echo "Hello, {$name}!\n";
Der Standardmodus gibt eine Begrüßung aus. Die anderen beiden Modi liefern echte Fehlerfälle in
Unterprozessen, die wir prüfen können: fail beendet sich mit Status 23,
während slow fünf Sekunden wartet, bevor eine Ausgabe erfolgt.
Das Skript aus PHP ausführen
Speichern Sie dies als php-external-scripts/run-script.php:
<?php
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Process;
require __DIR__ . '/vendor/autoload.php';
$nameArgument = $argv[1] ?? 'Developer';
$mode = $argv[2] ?? 'hello';
if (!in_array($mode, ['hello', 'fail', 'slow'], true)) {
fwrite(STDERR, "Usage: run-script.php [name] [hello|fail|slow]\n");
exit(64);
}
$process = new Process(
[PHP_BINARY, '-n', __DIR__ . '/hello.php', $nameArgument, $mode],
__DIR__
);
$process->setTimeout(1);
try {
$process->mustRun();
echo $process->getOutput();
} catch (ProcessTimedOutException $exception) {
fwrite(STDERR, "Child exceeded the 1-second timeout.\n");
exit(124);
} catch (ProcessFailedException $exception) {
fwrite(STDERR, sprintf("Child exited with status %d.\n", $process->getExitCode()));
exit(1);
}
Die Konstante PHP_BINARY
wählt die ausführbare PHP-Datei, mit der dieses CLI-Skript läuft, statt ein zweites
php in PATH zu suchen.
Der absolute Pfad des Kindskripts und dessen Arbeitsverzeichnis stammen aus
__DIR__, nicht aus Eingaben des Aufrufers.
Der lokale Autoloader von Composer stellt die Process-Klassen bereit.
mustRun() löst ProcessFailedException aus,
wenn der Kindprozess einen Exitcode ungleich null zurückgibt. Wir geben die erfasste
Standardausgabe erst nach erfolgreichem Abschluss aus. Ein Timeout für die Gesamtlaufzeit löst eine
separate Ausnahme vom Typ ProcessTimedOutException aus. Eine Sekunde ist für diese Demonstration
bewusst kurz gewählt. Setzen Sie für Ihre tatsächliche Aufgabe daher eine realistische Zeitgrenze.
Führen Sie Folgendes aus, während Sie sich weiterhin im übergeordneten Verzeichnis befinden:
php -n php-external-scripts/run-script.php
Erwartete Standardausgabe bei Exitstatus 0:
Hello, Developer!
Um zu sehen, warum getrennte Argumente wichtig sind, übergeben Sie einen Namen mit Shell-Syntax:
php -n php-external-scripts/run-script.php 'Developer; touch SHOULD_NOT_EXIST'
Erwartete Standardausgabe:
Hello, Developer; touch SHOULD_NOT_EXIST!
Das Semikolon ist Teil des Namens, kein Befehlstrennzeichen. Dieser Aufruf erstellt
SHOULD_NOT_EXIST nicht. Behalten Sie diese Trennung bei, wenn Sie das Beispiel anpassen:
Fügen Sie keinen Namen in eine Befehlszeichenfolge ein und wechseln Sie zur Datenübergabe nicht zu
Process::fromShellCommandline().
Praxisbeispiele
Einen Fehler des Kindprozesses prüfen
Führen Sie dieselben gespeicherten Dateien im Fehlermodus aus:
php -n php-external-scripts/run-script.php Developer fail
Das Aufrufskript schreibt Folgendes nach stderr und beendet sich mit Status 1, ohne eine Begrüßung auszugeben:
Child exited with status 23.
Der Status des Kindprozesses und der Status des Aufrufskripts unterscheiden sich bewusst. Dieser CLI-Wrapper meldet jeden Exit des Kindprozesses ungleich null mit seinem eigenen Status 1. Er täuscht keinen Erfolg der Aufgabe vor und leitet die Fehlerausgabe des Kindprozesses nicht an seinen Aufrufer weiter.
Einen Timeout prüfen
php -n php-external-scripts/run-script.php Developer slow
Das Aufrufskript schreibt Folgendes nach stderr und beendet sich mit Status 124:
Child exceeded the 1-second timeout.
Symfony prüft während des Wartens den Timeout für die Gesamtlaufzeit und stoppt diesen Kindprozess, bevor dessen fünfsekündige Pause endet. Es gibt keine Begrüßung. Wir setzen keinen Inaktivitäts-Timeout: Ein Kindprozess ohne Ausgabe ist nicht zwangsläufig defekt.
Fehlerbehandlung und bewährte Verfahren
Sicherheitsaspekte
Symfony empfiehlt ein Argument-Array als Befehlsform.
Bei diesem Vorgehen unter Linux werden die Argumente ohne Shell-Interpolation übergeben.
Für einzelne Array-Elemente benötigen Sie kein escapeshellarg().
Das verhindert Shell-Injection an dieser Schnittstelle, aber nicht jedes Risiko der Befehlsausführung:
- Behalten Sie die ausführbare Datei und das Skript unter Kontrolle der Anwendung. Dieses Beispiel akzeptiert eine Begrüßung und eine kleine Positivliste von Modi, keinen Befehl oder Skriptpfad.
- Validieren Sie Argumente entsprechend dem Kindprogramm. Ein anderes Tool könnte einen führenden
Bindestrich als Option interpretieren oder spezielle Dateinamensyntax erkennen, selbst wenn die
Shell sie nie sieht. Verwenden Sie
--nur, wenn das Tool dies als Markierung für das Ende der Optionen dokumentiert. - Führen Sie vertrauenswürdigen Code mit angemessenen Berechtigungen aus. Process isoliert weder Dateisystemzugriffe noch Netzwerkzugriffe oder Zugangsdaten. Verwenden Sie dieses Beispiel nicht, um hochgeladenen oder anderweitig nicht vertrauenswürdigen Code auszuführen.
Umfassende Fehlerbehandlung
Die beiden Catch-Blöcke unterscheiden einen fehlgeschlagenen Kindprozess von einer überschrittenen
Zeitgrenze. Ungültige Modi werden vor dem Start eines Kindprozesses mit einem Nutzungshinweis und
Status 64 abgewiesen. Fehlende Abhängigkeiten, ein fehlendes Skript oder ein nicht verfügbares
proc_open weisen auf ein Einrichtungsproblem hin, nicht auf einen erfolgreichen
Vorgang.
Geben Sie in einer Webanwendung eine feste, bereinigte Fehlermeldung zurück, statt Ausnahmemeldungen, Stacktraces oder erfasste Ausgaben des Kindprozesses offenzulegen. Entfernen Sie sensible Angaben aus sämtlichen serverseitigen Diagnosedaten. Dieses Tutorial ist ein lokales CLI-Beispiel und behandelt keine Anfrageverarbeitung mit PHP-FPM.
Ressourcenverwaltung
Der Timeout für die Gesamtlaufzeit begrenzt, wie lange das Aufrufskript auf diesen einfachen Kindprozess wartet. Er ist weder eine Speichergrenze noch eine Begrenzung der Ausgabegröße und garantiert nicht, dass beliebige Programme keine weiteren Kindprozesse oder unvollständigen Dateien zurücklassen. Die Begrüßung erzeugt nur eine kleine Ausgabe. Ein Programm mit umfangreicher Ausgabe benötigt ein separates Konzept für Streaming oder die Begrenzung der Ausgabe.
Verwenden Sie für dauerhafte Hintergrundarbeit eine Job-Queue oder einen Dienst-Supervisor. Einen Prozess asynchron zu starten bedeutet nicht, dass er seinen Elternprozess überlebt, wie die Process-Dokumentation erklärt. Bildkonvertierung und geplante Jobs benötigen eigene Richtlinien für Eingabe, Ausgabe und Bereitstellung. Diese Anleitung liefert solche Abläufe nicht.
Fazit
Bevor Sie hello.php durch eine echte Aufgabe ersetzen, legen Sie fest, welche
Argumente diese akzeptiert, was als Erfolg gilt und was nach einem Fehler oder Timeout geschehen
soll. Behalten Sie diese Prüfungen neben dem Argument-Array bei, statt sichere Maskierung als
vollständige Sicherheitsrichtlinie zu betrachten.
