Issues (1236)

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/command_functions.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
3
use Composer\Autoload\ClassLoader;
4
use Psalm\Config;
5
6
/**
7
 * @param  string $current_dir
8
 * @param  bool   $has_explicit_root
9
 * @param  string $vendor_dir
10
 *
11
 * @return ?\Composer\Autoload\ClassLoader
0 ignored issues
show
The doc-type ?\Composer\Autoload\ClassLoader could not be parsed: Unknown type name "?\Composer\Autoload\ClassLoader" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
12
 */
13
function requireAutoloaders($current_dir, $has_explicit_root, $vendor_dir)
14
{
15
    $autoload_roots = [$current_dir];
16
17
    $psalm_dir = dirname(__DIR__);
18
19
    /** @psalm-suppress UndefinedConstant */
20
    $in_phar = Phar::running() || strpos(__NAMESPACE__, 'HumbugBox');
21
22
    if ($in_phar) {
23
        require_once(__DIR__ . '/../vendor/autoload.php');
24
25
        // hack required for JsonMapper
26
        require_once __DIR__ . '/../vendor/netresearch/jsonmapper/src/JsonMapper.php';
27
        require_once __DIR__ . '/../vendor/netresearch/jsonmapper/src/JsonMapper/Exception.php';
28
    }
29
30
    if (realpath($psalm_dir) !== realpath($current_dir) && !$in_phar) {
31
        $autoload_roots[] = $psalm_dir;
32
    }
33
34
    $autoload_files = [];
35
36
    foreach ($autoload_roots as $autoload_root) {
37
        $has_autoloader = false;
38
39
        $nested_autoload_file = dirname(dirname($autoload_root)) . DIRECTORY_SEPARATOR . 'autoload.php';
40
41
        // note: don't realpath $nested_autoload_file, or phar version will fail
42
        if (file_exists($nested_autoload_file)) {
43
            if (!in_array($nested_autoload_file, $autoload_files, false)) {
44
                $autoload_files[] = $nested_autoload_file;
45
            }
46
            $has_autoloader = true;
47
        }
48
49
        $vendor_autoload_file =
50
            $autoload_root . DIRECTORY_SEPARATOR . $vendor_dir . DIRECTORY_SEPARATOR . 'autoload.php';
51
52
        // note: don't realpath $vendor_autoload_file, or phar version will fail
53
        if (file_exists($vendor_autoload_file)) {
54
            if (!in_array($vendor_autoload_file, $autoload_files, false)) {
55
                $autoload_files[] = $vendor_autoload_file;
56
            }
57
            $has_autoloader = true;
58
        }
59
60
        if (!$has_autoloader && file_exists($autoload_root . '/composer.json')) {
61
            $error_message = 'Could not find any composer autoloaders in ' . $autoload_root;
62
63
            if (!$has_explicit_root) {
64
                $error_message .= PHP_EOL . 'Add a --root=[your/project/directory] flag '
65
                    . 'to specify a particular project to run Psalm on.';
66
            }
67
68
            fwrite(STDERR, $error_message . PHP_EOL);
69
            exit(1);
70
        }
71
    }
72
73
    $first_autoloader = null;
74
75
    foreach ($autoload_files as $file) {
76
        /**
77
         * @psalm-suppress UnresolvableInclude
78
         *
79
         * @var mixed
80
         */
81
        $autoloader = require_once $file;
82
83
        if (!$first_autoloader
84
            && $autoloader instanceof ClassLoader
0 ignored issues
show
The class Composer\Autoload\ClassLoader does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
85
        ) {
86
            $first_autoloader = $autoloader;
87
        }
88
    }
89
90
    if ($first_autoloader === null && !$in_phar) {
91
        if (!$autoload_files) {
92
            fwrite(STDERR, 'Failed to find a valid Composer autoloader' . "\n");
93
        } else {
94
            fwrite(STDERR, 'Failed to find a valid Composer autoloader in ' . implode(', ', $autoload_files) . "\n");
95
        }
96
97
        fwrite(
98
            STDERR,
99
            'Please make sure you’ve run `composer install` in the current directory before using Psalm.' . "\n"
100
        );
101
        exit(1);
102
    }
103
104
    define('PSALM_VERSION', (string)\PackageVersions\Versions::getVersion('vimeo/psalm'));
105
    define('PHP_PARSER_VERSION', \PackageVersions\Versions::getVersion('nikic/php-parser'));
106
107
    return $first_autoloader;
108
}
109
110
/**
111
 * @param  string $current_dir
112
 *
113
 * @return string
114
 *
115
 * @psalm-suppress MixedArrayAccess
116
 * @psalm-suppress MixedAssignment
117
 * @psalm-suppress PossiblyUndefinedStringArrayOffset
118
 */
119
function getVendorDir($current_dir)
120
{
121
    $composer_json_path = $current_dir . DIRECTORY_SEPARATOR . 'composer.json';
122
123
    if (!file_exists($composer_json_path)) {
124
        return 'vendor';
125
    }
126
127
    if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) {
128
        fwrite(
129
            STDERR,
130
            'Invalid composer.json at ' . $composer_json_path . "\n"
131
        );
132
        exit(1);
133
    }
134
135
    if (isset($composer_json['config'])
136
        && is_array($composer_json['config'])
137
        && isset($composer_json['config']['vendor-dir'])
138
        && is_string($composer_json['config']['vendor-dir'])
139
    ) {
140
        return $composer_json['config']['vendor-dir'];
141
    }
142
143
    return 'vendor';
144
}
145
146
/**
147
 * @return string[]
148
 */
149
function getArguments() : array
150
{
151
    global $argv;
152
153
    if (!$argv) {
154
        return [];
155
    }
156
157
    $filtered_input_paths = [];
158
159
    for ($i = 0; $i < count($argv); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
160
        $input_path = $argv[$i];
161
162
        if (realpath($input_path) !== false) {
163
            continue;
164
        }
165
166
        if ($input_path[0] === '-' && strlen($input_path) === 2) {
167
            if ($input_path[1] === 'c' || $input_path[1] === 'f') {
168
                ++$i;
169
            }
170
            continue;
171
        }
172
173
        if ($input_path[0] === '-' && $input_path[2] === '=') {
174
            continue;
175
        }
176
177
        $filtered_input_paths[] = $input_path;
178
    }
179
180
    return $filtered_input_paths;
181
}
182
183
/**
184
 * @param  string|array|null|false $f_paths
185
 *
186
 * @return string[]|null
187
 */
188
function getPathsToCheck($f_paths)
189
{
190
    global $argv;
191
192
    $paths_to_check = [];
193
194
    if ($f_paths) {
195
        $input_paths = is_array($f_paths) ? $f_paths : [$f_paths];
196
    } else {
197
        $input_paths = $argv ? $argv : null;
198
    }
199
200
    if ($input_paths) {
201
        $filtered_input_paths = [];
202
203
        for ($i = 0; $i < count($input_paths); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
204
            /** @var string */
205
            $input_path = $input_paths[$i];
206
207
            if (realpath($input_path) === realpath(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'psalm')
208
                || realpath($input_path) === realpath(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'psalter')
209
                || realpath($input_path) === realpath(Phar::running(false))
210
            ) {
211
                continue;
212
            }
213
214
            if ($input_path[0] === '-' && strlen($input_path) === 2) {
215
                if ($input_path[1] === 'c' || $input_path[1] === 'f') {
216
                    ++$i;
217
                }
218
                continue;
219
            }
220
221
            if ($input_path[0] === '-' && $input_path[2] === '=') {
222
                continue;
223
            }
224
225
            if (substr($input_path, 0, 2) === '--' && strlen($input_path) > 2) {
226
                continue;
227
            }
228
229
            $filtered_input_paths[] = $input_path;
230
        }
231
232
        if ($filtered_input_paths === ['-']) {
233
            $meta = stream_get_meta_data(STDIN);
234
            stream_set_blocking(STDIN, false);
235
            if ($stdin = fgets(STDIN)) {
236
                $filtered_input_paths = preg_split('/\s+/', trim($stdin));
237
            }
238
            $blocked = $meta['blocked'];
239
            stream_set_blocking(STDIN, $blocked);
240
        }
241
242
        foreach ($filtered_input_paths as $path_to_check) {
243
            if ($path_to_check[0] === '-') {
244
                fwrite(STDERR, 'Invalid usage, expecting psalm [options] [file...]' . PHP_EOL);
245
                exit(1);
246
            }
247
248
            if (!file_exists($path_to_check)) {
249
                fwrite(STDERR, 'Cannot locate ' . $path_to_check . PHP_EOL);
250
                exit(1);
251
            }
252
253
            $path_to_check = realpath($path_to_check);
254
255
            if (!$path_to_check) {
256
                fwrite(STDERR, 'Error getting realpath for file' . PHP_EOL);
257
                exit(1);
258
            }
259
260
            $paths_to_check[] = $path_to_check;
261
        }
262
263
        if (!$paths_to_check) {
264
            $paths_to_check = null;
265
        }
266
    }
267
268
    return $paths_to_check;
269
}
270
271
function getPsalmHelpText(): string
272
{
273
    return <<<HELP
274
Usage:
275
    psalm [options] [file...]
276
277
Basic configuration:
278
    -c, --config=psalm.xml
279
        Path to a psalm.xml configuration file. Run psalm --init to create one.
280
281
    --use-ini-defaults
282
        Use PHP-provided ini defaults for memory and error display
283
284
    --disable-extension=[extension]
285
        Used to disable certain extensions while Psalm is running.
286
287
    --threads=INT
288
        If greater than one, Psalm will run analysis on multiple threads, speeding things up.
289
290
    --diff
291
        Runs Psalm in diff mode, only checking files that have changed since last run (and their dependents)
292
293
    --diff-methods
294
        Only checks methods that have changed since last run (and their dependents)
295
296
Surfacing issues:
297
    --show-info[=BOOLEAN]
298
        Show non-exception parser findings (defaults to false).
299
300
    --show-snippet[=true]
301
        Show code snippets with errors. Options are 'true' or 'false'
302
303
    --find-dead-code[=auto]
304
    --find-unused-code[=auto]
305
        Look for unused code. Options are 'auto' or 'always'. If no value is specified, default is 'auto'
306
307
    --find-unused-psalm-suppress
308
        Finds all @psalm-suppress annotations that aren’t used
309
310
    --find-references-to=[class|method|property]
311
        Searches the codebase for references to the given fully-qualified class or method,
312
        where method is in the format class::methodName
313
314
    --no-suggestions
315
        Hide suggestions
316
317
    --taint-analysis
318
        Run Psalm in taint analysis mode – see https://psalm.dev/docs/security_analysis for more info
319
320
Issue baselines:
321
    --set-baseline=PATH
322
        Save all current error level issues to a file, to mark them as info in subsequent runs
323
324
        Add --include-php-versions to also include a list of PHP extension versions
325
326
    --use-baseline=PATH
327
        Allows you to use a baseline other than the default baseline provided in your config
328
329
    --ignore-baseline
330
        Ignore the error baseline
331
332
    --update-baseline
333
        Update the baseline by removing fixed issues. This will not add new issues to the baseline
334
335
        Add --include-php-versions to also include a list of PHP extension versions
336
337
Plugins:
338
    --plugin=PATH
339
        Executes a plugin, an alternative to using the Psalm config
340
341
Output:
342
    -m, --monochrome
343
        Enable monochrome output
344
345
    --output-format=console
346
        Changes the output format.
347
        Available formats: compact, console, text, emacs, json, pylint, xml, checkstyle, junit, sonarqube, github
348
349
    --no-progress
350
        Disable the progress indicator
351
352
    --long-progress
353
        Use a progress indicator suitable for Continuous Integration logs
354
355
    --stats
356
        Shows a breakdown of Psalm's ability to infer types in the codebase
357
358
Reports:
359
    --report=PATH
360
        The path where to output report file. The output format is based on the file extension.
361
        (Currently supported formats: ".json", ".xml", ".txt", ".emacs", ".pylint", ".console",
362
        "checkstyle.xml", "sonarqube.json", "summary.json", "junit.xml")
363
364
    --report-show-info[=BOOLEAN]
365
        Whether the report should include non-errors in its output (defaults to true)
366
367
Caching:
368
    --clear-cache
369
        Clears all cache files that Psalm uses for this specific project
370
371
    --clear-global-cache
372
        Clears all cache files that Psalm uses for all projects
373
374
    --no-cache
375
        Runs Psalm without using cache
376
377
    --no-reflection-cache
378
        Runs Psalm without using cached representations of unchanged classes and files.
379
        Useful if you want the afterClassLikeVisit plugin hook to run every time you visit a file.
380
381
    --no-file-cache
382
        Runs Psalm without using caching every single file for later diffing.
383
        This reduces the space Psalm uses on disk and file I/O.
384
385
Miscellaneous:
386
    -h, --help
387
        Display this help message
388
389
    -v, --version
390
        Display the Psalm version
391
392
    -i, --init [source_dir=src] [level=3]
393
        Create a psalm config file in the current directory that points to [source_dir]
394
        at the required level, from 1, most strict, to 8, most permissive.
395
396
    --debug
397
        Debug information
398
399
    --debug-by-line
400
        Debug information on a line-by-line level
401
402
    --debug-emitted-issues
403
        Print a php backtrace to stderr when emitting issues.
404
405
    -r, --root
406
        If running Psalm globally you'll need to specify a project root. Defaults to cwd
407
408
    --generate-json-map=PATH
409
        Generate a map of node references and types in JSON format, saved to the given path.
410
411
    --generate-stubs=PATH
412
        Generate stubs for the project and dump the file in the given path
413
414
    --shepherd[=host]
415
        Send data to Shepherd, Psalm's GitHub integration tool.
416
417
    --alter
418
        Run Psalter
419
420
    --language-server
421
        Run Psalm Language Server
422
423
HELP;
424
}
425
426
function initialiseConfig(
427
    ?string $path_to_config,
428
    string $current_dir,
429
    string $output_format,
430
    ?ClassLoader $first_autoloader
431
): Config {
432
    try {
433
        if ($path_to_config) {
434
            $config = Config::loadFromXMLFile($path_to_config, $current_dir);
435
        } else {
436
            $config = Config::getConfigForPath($current_dir, $current_dir, $output_format);
437
        }
438
    } catch (Psalm\Exception\ConfigException $e) {
439
        fwrite(STDERR, $e->getMessage() . PHP_EOL);
440
        exit(1);
441
    }
442
443
    $config->setComposerClassLoader($first_autoloader);
444
445
    return $config;
446
}
447
448
function update_config_file(Config $config, string $config_file_path, string $baseline_path) : void
449
{
450
    if ($config->error_baseline === $baseline_path) {
451
        return;
452
    }
453
454
    $configFile = $config_file_path;
455
456
    if (is_dir($config_file_path)) {
457
        $configFile = Config::locateConfigFile($config_file_path);
458
    }
459
460
    if (!$configFile) {
461
        fwrite(STDERR, "Don't forget to set errorBaseline=\"{$baseline_path}\" to your config.");
462
463
        return;
464
    }
465
466
    $configFileContents = file_get_contents($configFile);
467
468
    if ($config->error_baseline) {
469
        $amendedConfigFileContents = preg_replace(
470
            '/errorBaseline=".*?"/',
471
            "errorBaseline=\"{$baseline_path}\"",
472
            $configFileContents
473
        );
474
    } else {
475
        $endPsalmOpenTag = strpos($configFileContents, '>', (int)strpos($configFileContents, '<psalm'));
476
477
        if (!$endPsalmOpenTag) {
478
            fwrite(STDERR, " Don't forget to set errorBaseline=\"{$baseline_path}\" in your config.");
479
            return;
480
        }
481
482
        if ($configFileContents[$endPsalmOpenTag - 1] === "\n") {
483
            $amendedConfigFileContents = substr_replace(
484
                $configFileContents,
485
                "    errorBaseline=\"{$baseline_path}\"\n>",
486
                $endPsalmOpenTag,
487
                1
488
            );
489
        } else {
490
            $amendedConfigFileContents = substr_replace(
491
                $configFileContents,
492
                " errorBaseline=\"{$baseline_path}\">",
493
                $endPsalmOpenTag,
494
                1
495
            );
496
        }
497
    }
498
499
    file_put_contents($configFile, $amendedConfigFileContents);
500
}
501
502
function get_path_to_config(array $options): ?string
503
{
504
    $path_to_config = isset($options['c']) && is_string($options['c']) ? realpath($options['c']) : null;
505
506
    if ($path_to_config === false) {
507
        fwrite(STDERR, 'Could not resolve path to config ' . (string) ($options['c'] ?? '') . PHP_EOL);
508
        exit(1);
509
    }
510
    return $path_to_config;
511
}
512
513
function getMemoryLimitInBytes(): int
514
{
515
    $limit = ini_get('memory_limit');
516
    // for unlimited = -1
517
    if ($limit < 0) {
518
        return -1;
519
    }
520
521
    if (preg_match('/^(\d+)(\D?)$/', $limit, $matches)) {
522
        $limit = (int)$matches[1];
523
        switch (strtoupper($matches[2] ?? '')) {
524
            case 'G':
525
                $limit *= 1024 * 1024 * 1024;
526
                break;
527
            case 'M':
528
                $limit *= 1024 * 1024;
529
                break;
530
            case 'K':
531
                $limit *= 1024;
532
                break;
533
        }
534
    }
535
536
    return (int)$limit;
537
}
538