Issues (1490)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Helper/CommandExecutor.php (13 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Fabrica\Helper;
4
5
use Exception;
6
use Fabrica\Tools\Logging\BuildLogger;
7
use Symfony\Component\Process\Process;
8
9
/**
10
 * Handles running system commands with variables.
11
 */
12
class CommandExecutor implements CommandExecutorInterface
13
{
14
    /**
15
     * @var BuildLogger
16
     */
17
    protected $logger;
18
19
    /**
20
     * @var bool
21
     */
22
    protected $verbose;
23
24
    /**
25
     * @var array
26
     */
27
    protected $lastOutput;
28
29
    /**
30
     * @var string
31
     */
32
    protected $lastError;
33
34
    /**
35
     * @var bool
36
     */
37
    public $logExecOutput = true;
38
39
    /**
40
     * The path which findBinary will look in.
41
     *
42
     * @var string
43
     */
44
    protected $rootDir;
45
46
    /**
47
     * Current build path
48
     *
49
     * @var string
50
     */
51
    protected $buildPath;
52
53
    /**
54
     * Commands with no proper exit mechanism
55
     *
56
     * @var array
57
     */
58
    private static $noExitCommands = [
59
        'codecept',
60
    ];
61
62
    /**
63
     * Environment variables that should not be inherited
64
     *
65
     * @var array
66
     */
67
    private static $blacklistEnvVars = [
68
        'PHP_SELF',
69
        'SCRIPT_NAME',
70
        'SCRIPT_FILENAME',
71
        'PATH_TRANSLATED',
72
        'DOCUMENT_ROOT',
73
        'SHELL_VERBOSITY',
74
    ];
75
76
    /**
77
     * @param BuildLogger $logger
78
     * @param string      $rootDir
79
     * @param bool        $verbose
80
     */
81
    public function __construct(BuildLogger $logger, $rootDir, $verbose = false)
82
    {
83
        $this->logger     = $logger;
84
        $this->verbose    = $verbose;
85
        $this->lastOutput = [];
86
        $this->rootDir    = $rootDir;
87
    }
88
89
    /**
90
     * Executes shell commands.
91
     *
92
     * @param array $args
93
     *
94
     * @return bool Indicates success
95
     */
96
    public function executeCommand($args = [])
97
    {
98
        $this->lastOutput = [];
99
100
        $this->logger->logDebug('Args: ' . json_encode($args));
101
102
        $command = call_user_func_array('sprintf', $args);
103
104
        $this->logger->logDebug('Command: ' . $command);
105
106
        $withNoExit = '';
107
        foreach (self::$noExitCommands as $nec) {
108
            if (preg_match("/\b{$nec}\b/", $command)) {
109
                $withNoExit = $nec;
110
                break;
111
            }
112
        }
113
114
        $process = new Process($command, $this->buildPath);
115
        $process->setTimeout(86400);
116
117
        $env = $this->getDefaultEnv();
118
119
        if (!empty($withNoExit)) {
120
            $process->start(null, $env);
121
122
            $this->logger->logDebug("Assuming command '{$withNoExit}' does not exit properly");
123
            do {
124
                sleep(15);
125
                $response = [];
126
                exec("ps auxww | grep '{$withNoExit}' | grep -v grep", $response);
127
                $response = array_filter(
128
                    $response,
129
                    function ($a) {
130
                        return strpos($a, $this->buildPath) !== false;
131
                    }
132
                );
133
            } while (!empty($response));
134
            $process->stop();
135
            $status = 0;
136
        } else {
137
            $process->setIdleTimeout(600);
138
            $process->start(null, $env);
139
            $status = $process->wait();
140
        }
141
142
        $lastOutput = $this->replaceIllegalCharacters($process->getOutput());
143
        $lastError  = $this->replaceIllegalCharacters($process->getErrorOutput());
144
145
        $this->lastOutput = array_filter(explode(PHP_EOL, $lastOutput));
146
        $this->lastError  = $lastError;
147
148
        $shouldOutput = ($this->logExecOutput && ($this->verbose || 0 !== $status));
149
150
        if ($shouldOutput && !empty($this->lastOutput)) {
151
            $this->logger->log($this->lastOutput);
152
        }
153
154
        if (!empty($this->lastError)) {
155
            $this->logger->logFailure($this->lastError);
156
        }
157
158
        $isSuccess = false;
159
        if (0 === $status) {
160
            $isSuccess = true;
161
        }
162
163
        $this->logger->logDebug('Execution status: ' . $status);
164
165
        return $isSuccess;
166
    }
167
168
    /**
169
     * @param string $utf8String
170
     *
171
     * @return string
172
     */
173
    public function replaceIllegalCharacters($utf8String)
174
    {
175
        mb_substitute_character(0xFFFD); // is '�'
176
        $legalUtf8String = mb_convert_encoding($utf8String, 'utf8', 'utf8');
177
        $regexp          = '/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]' .
178
            '|[^\x{0}-\x{ffff}]/u'; // more than 3 byte UTF-8 sequences (unsupported in mysql)
179
180
        return preg_replace($regexp, '�', $legalUtf8String);
181
    }
182
183
    /**
184
     * Returns the output from the last command run.
185
     *
186
     * @return string
187
     */
188
    public function getLastOutput()
189
    {
190
        return implode(PHP_EOL, $this->lastOutput);
191
    }
192
193
    /**
194
     * Returns the stderr output from the last command run.
195
     *
196
     * @return string
197
     */
198
    public function getLastError()
199
    {
200
        return $this->lastError;
201
    }
202
203
    /**
204
     * @param string $binaryPath
205
     * @param string $binary
206
     *
207
     * @return string|false
208
     */
209 View Code Duplication
    protected function findBinaryByPath($binaryPath, $binary)
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
210
    {
211
        if (is_dir($binaryPath) && is_file($binaryPath . '/' . $binary)) {
212
            $this->logger->logDebug(sprintf('Found in %s (binary_path): %s', $binaryPath, $binary));
213
214
            return $binaryPath . '/' . $binary;
215
        }
216
217
        return false;
218
    }
219
220
    /**
221
     * @param string $composerBin
222
     * @param string $binary
223
     *
224
     * @return string|false
225
     */
226 View Code Duplication
    protected function findBinaryLocal($composerBin, $binary)
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
227
    {
228
        if (is_dir($composerBin) && is_file($composerBin . '/' . $binary)) {
229
            $this->logger->logDebug(sprintf('Found in %s (local): %s', $composerBin, $binary));
230
231
            return $composerBin . '/' . $binary;
232
        }
233
234
        return false;
235
    }
236
237
    /**
238
     * @param string $binary
239
     *
240
     * @return string|false
241
     */
242
    protected function findBinaryGlobal($binary)
243
    {
244
        if (is_file($this->rootDir . 'vendor/bin/' . $binary)) {
245
            $this->logger->logDebug(sprintf('Found in %s (global): %s', 'vendor/bin', $binary));
246
247
            return $this->rootDir . 'vendor/bin/' . $binary;
248
        }
249
250
        return false;
251
    }
252
253
    /**
254
     * Uses 'which' to find a system binary by name
255
     *
256
     * @param string $binary
257
     *
258
     * @return string|false
259
     */
260
    protected function findBinarySystem($binary)
261
    {
262
        $tempBinary = trim(shell_exec('which ' . $binary));
263
        if (is_file($tempBinary)) {
264
            $this->logger->logDebug(sprintf('Found in %s (system): %s', '', $binary));
265
266
            return $tempBinary;
267
        }
268
269
        return false;
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function findBinary($binary, $priorityPath = 'local', $binaryPath = '', $binaryName = [])
276
    {
277
        $composerBin = $this->getComposerBinDir($this->buildPath);
278
279
        if (is_string($binary)) {
280
            $binary = [$binary];
281
        }
282
283
        if ($binaryName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $binaryName of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
284
            array_unshift($binary, ...$binaryName);
285
        }
286
287
        foreach ($binary as $bin) {
288
            $this->logger->logDebug(sprintf('Looking for binary: %s, priority = %s', $bin, $priorityPath));
289
290
            if ('binary_path' === $priorityPath) {
291
                if ($binaryPath = $this->findBinaryByPath($binaryPath, $bin)) {
0 ignored issues
show
It seems like $binaryPath defined by $this->findBinaryByPath($binaryPath, $bin) on line 291 can also be of type false; however, Fabrica\Helper\CommandExecutor::findBinaryByPath() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
292
                    return $binaryPath;
293
                }
294
295
                if ($binaryLocal = $this->findBinaryLocal($composerBin, $bin)) {
296
                    return $binaryLocal;
297
                }
298
299
                if ($binaryGlobal = $this->findBinaryGlobal($bin)) {
300
                    return $binaryGlobal;
301
                }
302
303
                if ($binarySystem = $this->findBinarySystem($bin)) {
304
                    return $binarySystem;
305
                }
306 View Code Duplication
            } elseif ('system' === $priorityPath) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
307
                if ($binarySystem = $this->findBinarySystem($bin)) {
308
                    return $binarySystem;
309
                }
310
311
                if ($binaryLocal = $this->findBinaryLocal($composerBin, $bin)) {
312
                    return $binaryLocal;
313
                }
314
315
                if ($binaryGlobal = $this->findBinaryGlobal($bin)) {
316
                    return $binaryGlobal;
317
                }
318
319
                if ($binaryPath = $this->findBinaryByPath($binaryPath, $bin)) {
0 ignored issues
show
It seems like $binaryPath defined by $this->findBinaryByPath($binaryPath, $bin) on line 319 can also be of type false; however, Fabrica\Helper\CommandExecutor::findBinaryByPath() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
320
                    return $binaryPath;
321
                }
322
            } elseif ('global' === $priorityPath) {
323
                if ($binaryGlobal = $this->findBinaryGlobal($bin)) {
324
                    return $binaryGlobal;
325
                }
326
327
                if ($binaryLocal = $this->findBinaryLocal($composerBin, $bin)) {
328
                    return $binaryLocal;
329
                }
330
331
                if ($binarySystem = $this->findBinarySystem($bin)) {
332
                    return $binarySystem;
333
                }
334
335
                if ($binaryPath = $this->findBinaryByPath($binaryPath, $bin)) {
0 ignored issues
show
It seems like $binaryPath defined by $this->findBinaryByPath($binaryPath, $bin) on line 335 can also be of type false; however, Fabrica\Helper\CommandExecutor::findBinaryByPath() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
336
                    return $binaryPath;
337
                }
338 View Code Duplication
            } else {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
339
                if ($binaryLocal = $this->findBinaryLocal($composerBin, $bin)) {
340
                    return $binaryLocal;
341
                }
342
343
                if ($binaryGlobal = $this->findBinaryGlobal($bin)) {
344
                    return $binaryGlobal;
345
                }
346
347
                if ($binarySystem = $this->findBinarySystem($bin)) {
348
                    return $binarySystem;
349
                }
350
351
                if ($binaryPath = $this->findBinaryByPath($binaryPath, $bin)) {
0 ignored issues
show
It seems like $binaryPath defined by $this->findBinaryByPath($binaryPath, $bin) on line 351 can also be of type false; however, Fabrica\Helper\CommandExecutor::findBinaryByPath() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
352
                    return $binaryPath;
353
                }
354
            }
355
        }
356
357
        throw new Exception(sprintf('Could not find %s', implode('/', $binary)));
358
    }
359
360
    /**
361
     * Try to load the composer.json file in the building project
362
     * If the bin-dir is configured, return the full path to it
363
     *
364
     * @param string $path Current build path
365
     *
366
     * @return string|null
367
     */
368
    public function getComposerBinDir($path)
369
    {
370
        if (is_dir($path)) {
371
            $composer = $path . '/composer.json';
372
            if (is_file($composer)) {
373
                $json = json_decode(file_get_contents($composer));
374
375
                if (isset($json->config->{"bin-dir"})) {
376
                    return $path . '/' . $json->config->{"bin-dir"};
377
                } elseif (is_dir($path . '/vendor/bin')) {
378
                    return $path . '/vendor/bin';
379
                }
380
            }
381
        }
382
        return null;
383
    }
384
385
    /**
386
     * Set the buildPath property.
387
     *
388
     * @param string $path
389
     */
390
    public function setBuildPath($path)
391
    {
392
        $this->buildPath = $path;
393
    }
394
395
396
397
398
    private function getDefaultEnv()
399
    {
400
        $env = array();
401
402
        foreach ($_SERVER as $k => $v) {
403
            if (in_array($k, self::$blacklistEnvVars)) {
404
                continue;
405
            }
406
            if (\is_string($v) && false !== $v = getenv($k)) {
407
                $env[$k] = $v;
408
            }
409
        }
410
411 View Code Duplication
        foreach ($_ENV as $k => $v) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
412
            if (in_array($k, self::$blacklistEnvVars)) {
413
                continue;
414
            }
415
            if (\is_string($v)) {
416
                $env[$k] = $v;
417
            }
418
        }
419
420
        if (PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 1) {
421 View Code Duplication
            foreach (getenv() as $k => $v) {
0 ignored issues
show
The expression getenv() of type string is not traversable.
Loading history...
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
422
                if (in_array($k, self::$blacklistEnvVars)) {
423
                    continue;
424
                }
425
                if (\is_string($v)) {
426
                    $env[$k] = $v;
427
                }
428
            }
429
        } else {
430
            $output = [];
431
            exec('env', $output);
432
            foreach ($output as $o) {
0 ignored issues
show
The expression $output of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
433
                $keyval = explode('=', $o, 2);
434
                if (count($keyval) < 2 || empty($keyval[1])) {
435
                    continue;
436
                }
437
                if (in_array($keyval[0], self::$blacklistEnvVars)) {
438
                    continue;
439
                }
440
                $env[$keyval[0]] = $keyval[1];
441
            }
442
        }
443
444
        return $env;
445
    }
446
}
447