Issues (7)

Security Analysis    no request data  

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/NodeJsInstaller.php (4 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
namespace Mouf\NodeJsInstaller;
3
4
use Composer\Composer;
5
use Composer\IO\IOInterface;
6
use Composer\Util\RemoteFilesystem;
7
8
class NodeJsInstaller
9
{
10
11
    /**
12
     * @var IOInterface
13
     */
14
    private $io;
15
16
    protected $rfs;
17
18
    public function __construct(IOInterface $io, Composer $composer)
19
    {
20
        $this->io = $io;
21
        $this->rfs = new RemoteFilesystem($io, $composer->getConfig());
22
    }
23
24
    /**
25
     * Checks if NodeJS is installed globally.
26
     * If yes, will return the version number.
27
     * If no, will return null.
28
     *
29
     * Note: trailing "v" will be removed from version string.
30
     *
31
     * @return null|string
32
     */
33
    public function getNodeJsGlobalInstallVersion()
34
    {
35
        $returnCode = 0;
36
        $output = "";
37
38
        ob_start();
39
        $version = exec("nodejs -v 2>&1", $output, $returnCode);
40
        ob_end_clean();
41
42
        if ($returnCode !== 0) {
43
            ob_start();
44
            $version = exec("node -v 2>&1", $output, $returnCode);
45
            ob_end_clean();
46
47
            if ($returnCode !== 0) {
48
                return;
49
            }
50
        }
51
52
        return ltrim($version, "v");
53
    }
54
55
    /**
56
     * Returns the full path to NodeJS global install (if available).
57
     */
58
    public function getNodeJsGlobalInstallPath()
59
    {
60
        $pathToNodeJS = $this->getGlobalInstallPath("nodejs");
61
        if (!$pathToNodeJS) {
62
            $pathToNodeJS = $this->getGlobalInstallPath("node");
63
        }
64
65
        return $pathToNodeJS;
66
    }
67
68
    /**
69
     * Returns the full install path to a command
70
     * @param string $command
71
     */
72
    public function getGlobalInstallPath($command)
73
    {
74
        if (Environment::isWindows()) {
75
            $result = trim(shell_exec("where /F ".escapeshellarg($command)), "\n\r");
76
77
            // "Where" can return several lines.
78
            $lines = explode("\n", $result);
79
80
            return $lines[0];
81
        } else {
82
            // We want to get output from stdout, not from stderr.
83
            // Therefore, we use proc_open.
84
            $descriptorspec = array(
85
                0 => array("pipe", "r"),  // stdin
86
                1 => array("pipe", "w"),  // stdout
87
                2 => array("pipe", "w"),  // stderr
88
            );
89
            $pipes = array();
90
91
            $process = proc_open("which ".escapeshellarg($command), $descriptorspec, $pipes);
92
93
            $stdout = stream_get_contents($pipes[1]);
94
            fclose($pipes[1]);
95
96
            // Let's ignore stderr (it is possible we do not find anything and depending on the OS, stderr will
97
            // return things or not)
98
            fclose($pipes[2]);
99
100
            proc_close($process);
101
102
            return trim($stdout, "\n\r");
103
        }
104
    }
105
106
    /**
107
     * Checks if NodeJS is installed locally.
108
     * If yes, will return the version number.
109
     * If no, will return null.
110
     *
111
     * Note: trailing "v" will be removed from version string.
112
     *
113
     * @return null|string
114
     */
115
    public function getNodeJsLocalInstallVersion($binDir)
116
    {
117
        $returnCode = 0;
118
        $output = "";
119
120
        $cwd = getcwd();
121
        chdir(__DIR__.'/../../../../');
122
123
        ob_start();
124
125
        $version = exec($binDir.DIRECTORY_SEPARATOR.'node -v 2>&1', $output, $returnCode);
126
127
        ob_end_clean();
128
129
        chdir($cwd);
130
131
        if ($returnCode !== 0) {
132
            return;
133
        } else {
134
            return ltrim($version, "v");
135
        }
136
    }
137
138
    /**
139
     * Returns URL based on version.
140
     * URL is dependent on environment
141
     * @param  string                   $version
142
     * @return string
143
     * @throws NodeJsInstallerException
144
     */
145
    public function getNodeJSUrl($version)
146
    {
147
        if (Environment::isWindows() && Environment::getArchitecture() == 32) {
148 View Code Duplication
            if (version_compare($version, '4.0.0') >= 0) {
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...
149
                return "https://nodejs.org/dist/v".$version."/win-x86/node.exe";
150
            } else {
151
                return "https://nodejs.org/dist/v".$version."/node.exe";
152
            }
153
        } elseif (Environment::isWindows() && Environment::getArchitecture() == 64) {
154 View Code Duplication
            if (version_compare($version, '4.0.0') >= 0) {
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...
155
                return "https://nodejs.org/dist/v" . $version . "/win-x64/node.exe";
156
            } else {
157
                return "https://nodejs.org/dist/v" . $version . "/x64/node.exe";
158
            }
159
        } elseif (Environment::isMacOS() && Environment::getArchitecture() == 32) {
160
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-darwin-x86.tar.gz";
161
        } elseif (Environment::isMacOS() && Environment::getArchitecture() == 64) {
162
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-darwin-x64.tar.gz";
163
        } elseif (Environment::isSunOS() && Environment::getArchitecture() == 32) {
164
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-sunos-x86.tar.gz";
165
        } elseif (Environment::isSunOS() && Environment::getArchitecture() == 64) {
166
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-sunos-x64.tar.gz";
167
        } elseif (Environment::isLinux() && Environment::isArm()) {
168
            if (version_compare($version, '4.0.0') >= 0) {
169
                if (Environment::isArmV6l()) {
170
                    return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-armv6l.tar.gz";
171
                } elseif (Environment::isArmV7l()) {
172
                    return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-armv7l.tar.gz";
173
                } elseif (Environment::getArchitecture() == 64) {
174
                    return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-arm64.tar.gz";
175
                } else {
176
                    throw new NodeJsInstallerException('NodeJS-installer cannot install Node on computers with ARM 32bits processors that are not v6l or v7l. Please install NodeJS globally on your machine first, then run composer again.');
177
                }
178
            } else {
179
                throw new NodeJsInstallerException('NodeJS-installer cannot install Node <4.0 on computers with ARM processors. Please install NodeJS globally on your machine first, then run composer again, or consider installing a version of NodeJS >=4.0.');
180
            }
181
        } elseif (Environment::isLinux() && Environment::getArchitecture() == 32) {
182
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-x86.tar.gz";
183
        } elseif (Environment::isLinux() && Environment::getArchitecture() == 64) {
184
            return "https://nodejs.org/dist/v".$version."/node-v".$version."-linux-x64.tar.gz";
185
        } else {
186
            throw new NodeJsInstallerException('Unsupported architecture: '.PHP_OS.' - '.Environment::getArchitecture().' bits');
187
        }
188
    }
189
190
    /**
191
     * Installs NodeJS
192
     * @param  string                   $version
193
     * @param  string                   $targetDirectory
194
     * @throws NodeJsInstallerException
195
     */
196
    public function install($version, $targetDirectory)
197
    {
198
        $this->io->write("Installing <info>NodeJS v".$version."</info>");
199
        $url = $this->getNodeJSUrl($version);
200
        $this->io->write("  Downloading from $url");
201
202
        $cwd = getcwd();
203
204
        $fileName = 'vendor/'.pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME);
205
206
        $this->rfs->copy(parse_url($url, PHP_URL_HOST), $url, $fileName);
0 ignored issues
show
It seems like parse_url($url, PHP_URL_HOST) targeting parse_url() can also be of type false; however, Composer\Util\RemoteFilesystem::copy() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
207
208
        if (!file_exists($fileName)) {
209
            throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
210
                .' directory is writable and you have internet connectivity');
211
        }
212
213
        if (!file_exists($targetDirectory)) {
214
            mkdir($targetDirectory, 0775, true);
215
        }
216
217
        if (!is_writable($targetDirectory)) {
218
            throw new NodeJsInstallerException("'$targetDirectory' is not writable");
219
        }
220
221
        if (!Environment::isWindows()) {
222
            // Now, if we are not in Windows, let's untar.
223
            $this->extractTo($fileName, $targetDirectory);
224
225
            // Let's delete the downloaded file.
226
            unlink($fileName);
227
        } else {
228
            // If we are in Windows, let's move and install NPM.
229
            rename($fileName, $targetDirectory.'/'.basename($fileName));
230
231
            // We have to download the latest available version in a bin for Windows, then upgrade it:
232
            $url = "https://nodejs.org/dist/npm/npm-1.4.12.zip";
233
            $npmFileName = "vendor/npm-1.4.12.zip";
234
            $this->rfs->copy(parse_url($url, PHP_URL_HOST), $url, $npmFileName);
0 ignored issues
show
It seems like parse_url($url, PHP_URL_HOST) targeting parse_url() can also be of type false; however, Composer\Util\RemoteFilesystem::copy() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
235
236
            $this->unzip($npmFileName, $targetDirectory);
237
238
            unlink($npmFileName);
239
240
            // Let's update NPM
241
            $highestNpmVersion = "latest";
242
            if ($version < 10) {
243
                $highestNpmVersion = "6.14.8";
244
            }
245
246
            // 1- Update PATH to run npm.
247
            $path = getenv('PATH');
248
            $newPath = realpath($targetDirectory).";".$path;
249
            putenv('PATH='.$newPath);
250
251
            // 2- Run npm
252
            $cwd2 = getcwd();
253
            chdir($targetDirectory);
254
255
            $returnCode = 0;
256
            passthru("npm update npm@{$highestNpmVersion}", $returnCode);
257
            if ($returnCode !== 0) {
258
                throw new NodeJsInstallerException("An error occurred while updating NPM to latest version.");
259
            }
260
261
            // Finally, let's copy the base npm file for Cygwin
262
            if (file_exists('node_modules/npm/bin/npm')) {
263
                copy('node_modules/npm/bin/npm', 'npm');
264
            }
265
266
            chdir($cwd2);
267
        }
268
269
        chdir($cwd);
270
    }
271
272
    /**
273
     * Extract tar.gz file to target directory.
274
     *
275
     * @param string $tarGzFile
276
     * @param string $targetDir
277
     */
278
    private function extractTo($tarGzFile, $targetDir)
279
    {
280
        // Note: we cannot use PharData class because it does not keeps symbolic links.
281
        // Also, --strip 1 allows us to remove the first directory.
282
283
        $output = $return_var = null;
284
285
        exec("tar -xvf ".$tarGzFile." -C ".escapeshellarg($targetDir)." --strip 1", $output, $return_var);
286
287
        if ($return_var !== 0) {
288
            throw new NodeJsInstallerException("An error occurred while untaring NodeJS ($tarGzFile) to $targetDir");
289
        }
290
    }
291
292
    public function createBinScripts($binDir, $targetDir, $isLocal)
293
    {
294
        if (!file_exists($binDir)) {
295
            $result = mkdir($binDir, 0775, true);
296
            if ($result === false) {
297
                throw new NodeJsInstallerException("Unable to create directory ".$binDir);
298
            }
299
        }
300
301
        $fullTargetDir = realpath($targetDir);
302
        $binDir = realpath($binDir);
303
304
        if (!Environment::isWindows()) {
305
            $this->createBinScript($binDir, $fullTargetDir, 'node', 'node', $isLocal);
306
            $this->createBinScript($binDir, $fullTargetDir, 'npm', 'npm', $isLocal);
307
        } else {
308
            $this->createBinScript($binDir, $fullTargetDir, 'node.bat', 'node', $isLocal);
309
            $this->createBinScript($binDir, $fullTargetDir, 'npm.bat', 'npm', $isLocal);
310
        }
311
    }
312
313
    /**
314
     * Copy script into $binDir, replacing PATH with $fullTargetDir
315
     * @param string $binDir
316
     * @param string $fullTargetDir
317
     * @param string $scriptName
318
     * @param bool   $isLocal
319
     */
320
    private function createBinScript($binDir, $fullTargetDir, $scriptName, $target, $isLocal)
321
    {
322
        $content = file_get_contents(__DIR__.'/../bin/'.($isLocal ? "local/" : "global/").$scriptName);
323
        if ($isLocal) {
324
            $path = $this->makePathRelative($fullTargetDir, $binDir);
325
        } else {
326
            if ($scriptName == "node") {
327
                $path = $this->getNodeJsGlobalInstallPath();
328
            } else {
329
                $path = $this->getGlobalInstallPath($target);
330
            }
331
332
            if (strpos($path, $binDir) === 0) {
333
                // we found the local installation that already exists.
334
335
                return;
336
            }
337
        }
338
339
340
        file_put_contents($binDir.'/'.$scriptName, sprintf($content, $path));
341
        chmod($binDir.'/'.$scriptName, 0755);
342
    }
343
344
    /**
345
     * Shamelessly stolen from Symfony's FileSystem. Thanks guys!
346
     * Given an existing path, convert it to a path relative to a given starting path.
347
     *
348
     * @param string $endPath   Absolute path of target
349
     * @param string $startPath Absolute path where traversal begins
350
     *
351
     * @return string Path of target relative to starting path
352
     */
353
    private function makePathRelative($endPath, $startPath)
354
    {
355
        // Normalize separators on Windows
356
        if ('\\' === DIRECTORY_SEPARATOR) {
357
            $endPath = strtr($endPath, '\\', '/');
358
            $startPath = strtr($startPath, '\\', '/');
359
        }
360
        // Split the paths into arrays
361
        $startPathArr = explode('/', trim($startPath, '/'));
362
        $endPathArr = explode('/', trim($endPath, '/'));
363
        // Find for which directory the common path stops
364
        $index = 0;
365
        while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
366
            $index++;
367
        }
368
        // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
369
        $depth = count($startPathArr) - $index;
370
        // Repeated "../" for each level need to reach the common path
371
        $traverser = str_repeat('../', $depth);
372
        $endPathRemainder = implode('/', array_slice($endPathArr, $index));
373
        // Construct $endPath from traversing to the common path, then to the remaining $endPath
374
        $relativePath = $traverser.(strlen($endPathRemainder) > 0 ? $endPathRemainder.'/' : '');
375
376
        return (strlen($relativePath) === 0) ? './' : $relativePath;
377
    }
378
379
    private function unzip($zipFileName, $targetDir)
380
    {
381
        $zip = new \ZipArchive();
382
        $res = $zip->open($zipFileName);
383
        if ($res === true) {
384
            // extract it to the path we determined above
385
            $zip->extractTo($targetDir);
386
            $zip->close();
387
        } else {
388
            throw new NodeJsInstallerException("Unable to extract file $zipFileName");
389
        }
390
    }
391
392
    /**
393
     * Adds the vendor/bin directory into the path.
394
     * Note: the vendor/bin is prepended in order to be applied BEFORE an existing install of node.
395
     *
396
     * @param string $binDir
397
     */
398
    public function registerPath($binDir)
399
    {
400
        $path = getenv('PATH');
401
        if (Environment::isWindows()) {
402
            putenv('PATH='.realpath($binDir).';'.$path);
403
        } else {
404
            putenv('PATH='.realpath($binDir).':'.$path);
405
        }
406
    }
407
}
408