From 4fcc13d428f28479b01f757341aadd625412b454 Mon Sep 17 00:00:00 2001
From: Jordi Boggiano <j.boggiano@seld.be>
Date: Tue, 14 Apr 2026 11:29:10 +0200
Subject: [PATCH] Convert perforce util to use array process args to avoid
 injections

---
 src/Composer/Util/Perforce.php            | 66 +++++++++-------
 tests/Composer/Test/Util/PerforceTest.php | 93 +++++------------------
 2 files changed, 55 insertions(+), 104 deletions(-)

Index: SRC/src/Composer/Util/Perforce.php
===================================================================
--- SRC.orig/src/Composer/Util/Perforce.php
+++ SRC/src/Composer/Util/Perforce.php
@@ -135,7 +135,7 @@ return gethostname() . "_" . time();
 public function cleanupClientSpec(): void
 {
 $client = $this->getClient();
-$task = 'client -d ' . ProcessExecutor::escape($client);
+$task = ['client', '-d', $client];
 $useP4Client = false;
 $command = $this->generateP4Command($task, $useP4Client);
 $this->executeCommand($command);
@@ -308,21 +308,24 @@ return $password;
 
 
 
-public function generateP4Command(string $command, bool $useClient = true): string
+public function generateP4Command(array $arguments, bool $useClient = true): array
 {
-$p4Command = $this->getP4Executable().' ';
-$p4Command .= '-u ' . ProcessExecutor::escape($this->getUser()) . ' ';
+$p4Command = [$this->getP4Executable()];
+$p4Command[] = '-u';
+$p4Command[] = $this->getUser();
 if ($useClient) {
-$p4Command .= '-c ' . ProcessExecutor::escape($this->getClient()) . ' ';
+$p4Command[] = '-c';
+$p4Command[] = $this->getClient();
 }
-$p4Command .= '-p ' . ProcessExecutor::escape($this->getPort()) . ' ' . $command;
+$p4Command[] = '-p';
+$p4Command[] = $this->getPort();
 
-return $p4Command;
+return array_merge($p4Command, $arguments);
 }
 
 public function isLoggedIn(): bool
 {
-$command = $this->generateP4Command('login -s', false);
+$command = $this->generateP4Command(['login', '-s'], false);
 $exitCode = $this->executeCommand($command);
 if ($exitCode) {
 $errorOutput = $this->process->getErrorOutput();
@@ -342,19 +345,19 @@ return true;
 
 public function connectClient(): void
 {
-$p4CreateClientCommand = $this->generateP4Command(
-'client -i < ' . ProcessExecutor::escape($this->getP4ClientSpec())
-);
-$this->executeCommand($p4CreateClientCommand);
+$p4CreateClientCommand = $this->generateP4Command(['client', '-i']);
+
+$process = new Process($p4CreateClientCommand, null, null, file_get_contents($this->getP4ClientSpec()));
+$process->run();
 }
 
 public function syncCodeBase(?string $sourceReference): void
 {
 $prevDir = Platform::getCwd();
 chdir($this->path);
-$p4SyncCommand = $this->generateP4Command('sync -f ');
+$p4SyncCommand = $this->generateP4Command(['sync', '-f']);
 if (null !== $sourceReference) {
-$p4SyncCommand .= '@' . $sourceReference;
+$p4SyncCommand[] = '@' . $sourceReference;
 }
 $this->executeCommand($p4SyncCommand);
 chdir($prevDir);
@@ -416,9 +419,9 @@ $line = fgets($pipe);
 
 public function windowsLogin(?string $password): int
 {
-$command = $this->generateP4Command(' login -a');
+$command = $this->generateP4Command(['login', '-a']);
 
-$process = Process::fromShellCommandline($command, null, null, $password);
+$process = new Process($command, null, null, $password);
 
 return $process->run();
 }
@@ -431,9 +434,12 @@ $password = $this->queryP4Password();
 if ($this->windowsFlag) {
 $this->windowsLogin($password);
 } else {
-$command = 'echo ' . ProcessExecutor::escape($password) . ' | ' . $this->generateP4Command(' login -a', false);
-$exitCode = $this->executeCommand($command);
-if ($exitCode) {
+$command = $this->generateP4Command(['login', '-a'], false);
+
+$process = new Process($command, null, null, $password);
+$process->run();
+
+if (!$process->isSuccessful()) {
 throw new \Exception("Error logging in:" . $this->process->getErrorOutput());
 }
 }
@@ -458,7 +464,7 @@ public function getFileContent(string $f
 {
 $path = $this->getFilePath($file, $identifier);
 
-$command = $this->generateP4Command(' print ' . ProcessExecutor::escape($path));
+$command = $this->generateP4Command(['print', $path]);
 $this->executeCommand($command);
 $result = $this->commandResult;
 
@@ -477,7 +483,7 @@ return $identifier. '/' . $file;
 }
 
 $path = substr($identifier, 0, $index) . '/' . $file . substr($identifier, $index);
-$command = $this->generateP4Command(' files ' . ProcessExecutor::escape($path), false);
+$command = $this->generateP4Command(['files', $path], false);
 $this->executeCommand($command);
 $result = $this->commandResult;
 $index2 = strpos($result, 'no such file(s).');
@@ -503,7 +509,7 @@ $possibleBranches = [];
 if (!$this->isStream()) {
 $possibleBranches[$this->p4Branch] = $this->getStream();
 } else {
-$command = $this->generateP4Command('streams '.ProcessExecutor::escape('//' . $this->p4Depot . '/...'));
+$command = $this->generateP4Command(['streams', '//' . $this->p4Depot . '/...']);
 $this->executeCommand($command);
 $result = $this->commandResult;
 $resArray = explode(PHP_EOL, $result);
@@ -515,7 +521,7 @@ $possibleBranches[$branch] = $resBits[1]
 }
 }
 }
-$command = $this->generateP4Command('changes '. ProcessExecutor::escape($this->getStream() . '/...'), false);
+$command = $this->generateP4Command(['changes', $this->getStream() . '/...'], false);
 $this->executeCommand($command);
 $result = $this->commandResult;
 $resArray = explode(PHP_EOL, $result);
@@ -531,7 +537,7 @@ return ['master' => $possibleBranches[$t
 
 public function getTags(): array
 {
-$command = $this->generateP4Command('labels');
+$command = $this->generateP4Command(['labels']);
 $this->executeCommand($command);
 $result = $this->commandResult;
 $resArray = explode(PHP_EOL, $result);
@@ -548,7 +554,7 @@ return $tags;
 
 public function checkStream(): bool
 {
-$command = $this->generateP4Command('depots', false);
+$command = $this->generateP4Command(['depots'], false);
 $this->executeCommand($command);
 $result = $this->commandResult;
 $resArray = explode(PHP_EOL, $result);
@@ -576,7 +582,7 @@ if ($index === false) {
 return null;
 }
 $label = substr($reference, $index);
-$command = $this->generateP4Command(' changes -m1 ' . ProcessExecutor::escape($label));
+$command = $this->generateP4Command(['changes', '-m1', $label]);
 $this->executeCommand($command);
 $changes = $this->commandResult;
 if (strpos($changes, 'Change') !== 0) {
@@ -602,7 +608,7 @@ return null;
 }
 $index = strpos($fromReference, '@');
 $main = substr($fromReference, 0, $index) . '/...';
-$command = $this->generateP4Command('filelog ' . ProcessExecutor::escape($main . '@' . $fromChangeList. ',' . $toChangeList));
+$command = $this->generateP4Command(['filelog', $main . '@' . $fromChangeList . ',' . $toChangeList]);
 $this->executeCommand($command);
 
 return $this->commandResult;
