Issues (32)

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/Whoops/Run.php (10 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
 * Whoops - php errors for cool kids
4
 * @author Filipe Dobreira <http://github.com/filp>
5
 */
6
7
namespace Whoops;
8
9
use InvalidArgumentException;
10
use Throwable;
11
use Whoops\Exception\ErrorException;
12
use Whoops\Exception\Inspector;
13
use Whoops\Handler\CallbackHandler;
14
use Whoops\Handler\Handler;
15
use Whoops\Handler\HandlerInterface;
16
use Whoops\Util\Misc;
17
use Whoops\Util\SystemFacade;
18
19
final class Run implements RunInterface
20
{
21
    /**
22
     * @var bool
23
     */
24
    private $isRegistered;
25
26
    /**
27
     * @var bool
28
     */
29
    private $allowQuit       = true;
30
31
    /**
32
     * @var bool
33
     */
34
    private $sendOutput      = true;
35
36
    /**
37
     * @var integer|false
38
     */
39
    private $sendHttpCode    = 500;
40
41
    /**
42
     * @var integer|false
43
     */
44
    private $sendExitCode    = 1;
45
46
    /**
47
     * @var HandlerInterface[]
48
     */
49
    private $handlerStack = [];
50
51
    /**
52
     * @var array
53
     * @psalm-var list<array{patterns: string, levels: int}>
54
     */
55
    private $silencedPatterns = [];
56
57
    /**
58
     * @var SystemFacade
59
     */
60
    private $system;
61
62
    /**
63
     * In certain scenarios, like in shutdown handler, we can not throw exceptions.
64
     *
65
     * @var bool
66
     */
67
    private $canThrowExceptions = true;
68
69 6
    public function __construct(SystemFacade $system = null)
70
    {
71 6
        $this->system = $system ?: new SystemFacade;
72 6
    }
73
74
    /**
75
     * Explicitly request your handler runs as the last of all currently registered handlers.
76
     *
77
     * @param HandlerInterface $handler
78
     *
79
     * @return Run
80
     */
81
    public function appendHandler($handler)
82
    {
83
        array_unshift($this->handlerStack, $this->resolveHandler($handler));
84
        return $this;
85
    }
86
87
    /**
88
     * Explicitly request your handler runs as the first of all currently registered handlers.
89
     *
90
     * @param HandlerInterface $handler
91
     *
92
     * @return Run
93
     */
94
    public function prependHandler($handler)
95
    {
96
        return $this->pushHandler($handler);
97
    }
98
99
    /**
100
     * Register your handler as the last of all currently registered handlers (to be executed first).
101
     * Prefer using appendHandler and prependHandler for clarity.
102
     *
103
     * @param Callable|HandlerInterface $handler
104
     *
105
     * @return Run
106
     *
107
     * @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface.
108
     */
109 10
    public function pushHandler($handler)
110
    {
111 10
        $this->handlerStack[] = $this->resolveHandler($handler);
0 ignored issues
show
It seems like $handler defined by parameter $handler on line 109 can also be of type callable; however, Whoops\Run::resolveHandler() does only seem to accept object<Whoops\Handler\HandlerInterface>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
112 9
        return $this;
113
    }
114
115
    /**
116
     * Removes and returns the last handler pushed to the handler stack.
117
     *
118
     * @see Run::removeFirstHandler(), Run::removeLastHandler()
119
     *
120
     * @return HandlerInterface|null
121
     */
122 1
    public function popHandler()
123
    {
124 1
        return array_pop($this->handlerStack);
125
    }
126
127
    /**
128
     * Removes the first handler.
129
     *
130
     * @return void
131
     */
132 1
    public function removeFirstHandler()
133
    {
134 1
        array_pop($this->handlerStack);
135 1
    }
136
137
    /**
138
     * Removes the last handler.
139
     *
140
     * @return void
141
     */
142 1
    public function removeLastHandler()
143
    {
144 1
        array_shift($this->handlerStack);
145 1
    }
146
147
    /**
148
     * Returns an array with all handlers, in the order they were added to the stack.
149
     *
150
     * @return array
151
     */
152 3
    public function getHandlers()
153
    {
154 3
        return $this->handlerStack;
155
    }
156
157
    /**
158
     * Clears all handlers in the handlerStack, including the default PrettyPage handler.
159
     *
160
     * @return Run
161
     */
162 1
    public function clearHandlers()
163
    {
164 1
        $this->handlerStack = [];
165 1
        return $this;
166
    }
167
168
    /**
169
     * Registers this instance as an error handler.
170
     *
171
     * @return Run
172
     */
173 6
    public function register()
174
    {
175 6
        if (!$this->isRegistered) {
176
            // Workaround PHP bug 42098
177
            // https://bugs.php.net/bug.php?id=42098
178 6
            class_exists("\\Whoops\\Exception\\ErrorException");
179 6
            class_exists("\\Whoops\\Exception\\FrameCollection");
180 6
            class_exists("\\Whoops\\Exception\\Frame");
181 6
            class_exists("\\Whoops\\Exception\\Inspector");
182
183 6
            $this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
184 6
            $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
185 6
            $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
186
187 6
            $this->isRegistered = true;
188 6
        }
189
190 6
        return $this;
191
    }
192
193
    /**
194
     * Unregisters all handlers registered by this Whoops\Run instance.
195
     *
196
     * @return Run
197
     */
198 1
    public function unregister()
199
    {
200 1
        if ($this->isRegistered) {
201 1
            $this->system->restoreExceptionHandler();
202 1
            $this->system->restoreErrorHandler();
203
204 1
            $this->isRegistered = false;
205 1
        }
206
207 1
        return $this;
208
    }
209
210
    /**
211
     * Should Whoops allow Handlers to force the script to quit?
212
     *
213
     * @param bool|int $exit
214
     *
215
     * @return bool
216
     */
217 6
    public function allowQuit($exit = null)
218
    {
219 6
        if (func_num_args() == 0) {
220 3
            return $this->allowQuit;
221
        }
222
223 6
        return $this->allowQuit = (bool) $exit;
224
    }
225
226
    /**
227
     * Silence particular errors in particular files.
228
     *
229
     * @param array|string $patterns List or a single regex pattern to match.
230
     * @param int          $levels   Defaults to E_STRICT | E_DEPRECATED.
231
     *
232
     * @return Run
233
     */
234 1
    public function silenceErrorsInPaths($patterns, $levels = 10240)
235
    {
236 1
        $this->silencedPatterns = array_merge(
237 1
            $this->silencedPatterns,
238 1
            array_map(
239 1
                function ($pattern) use ($levels) {
240
                    return [
241 1
                        "pattern" => $pattern,
242 1
                        "levels" => $levels,
243 1
                    ];
244 1
                },
245
                (array) $patterns
246 1
            )
247 1
        );
248
249 1
        return $this;
250
    }
251
252
    /**
253
     * Returns an array with silent errors in path configuration.
254
     *
255
     * @return array
256
     */
257
    public function getSilenceErrorsInPaths()
258
    {
259
        return $this->silencedPatterns;
260
    }
261
262
    /**
263
     * Should Whoops send HTTP error code to the browser if possible?
264
     * Whoops will by default send HTTP code 500, but you may wish to
265
     * use 502, 503, or another 5xx family code.
266
     *
267
     * @param bool|int $code
268
     *
269
     * @return int|false
270
     *
271
     * @throws InvalidArgumentException
272
     */
273 6
    public function sendHttpCode($code = null)
274
    {
275 6
        if (func_num_args() == 0) {
276 4
            return $this->sendHttpCode;
277
        }
278
279 3
        if (!$code) {
280 1
            return $this->sendHttpCode = false;
281
        }
282
283 2
        if ($code === true) {
284 1
            $code = 500;
285 1
        }
286
287 2
        if ($code < 400 || 600 <= $code) {
288 1
            throw new InvalidArgumentException(
289 1
                "Invalid status code '$code', must be 4xx or 5xx"
290 1
            );
291
        }
292
293 1
        return $this->sendHttpCode = $code;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->sendHttpCode = $code; of type boolean|integer adds the type boolean to the return on line 293 which is incompatible with the return type declared by the interface Whoops\RunInterface::sendHttpCode of type integer|false.
Loading history...
294
    }
295
296
    /**
297
     * Should Whoops exit with a specific code on the CLI if possible?
298
     * Whoops will exit with 1 by default, but you can specify something else.
299
     *
300
     * @param int $code
301
     *
302
     * @return int
303
     *
304
     * @throws InvalidArgumentException
305
     */
306 2
    public function sendExitCode($code = null)
307
    {
308 2
        if (func_num_args() == 0) {
309 1
            return $this->sendExitCode;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->sendExitCode; of type integer|false adds false to the return on line 309 which is incompatible with the return type declared by the interface Whoops\RunInterface::sendExitCode of type integer. It seems like you forgot to handle an error condition.
Loading history...
310
        }
311
312 1
        if ($code < 0 || 255 <= $code) {
313 1
            throw new InvalidArgumentException(
314 1
                "Invalid status code '$code', must be between 0 and 254"
315 1
            );
316
        }
317
318
        return $this->sendExitCode = (int) $code;
319
    }
320
321
    /**
322
     * Should Whoops push output directly to the client?
323
     * If this is false, output will be returned by handleException.
324
     *
325
     * @param bool|int $send
326
     *
327
     * @return bool
328
     */
329 5
    public function writeToOutput($send = null)
330
    {
331 5
        if (func_num_args() == 0) {
332 5
            return $this->sendOutput;
333
        }
334
335 1
        return $this->sendOutput = (bool) $send;
336
    }
337
338
    /**
339
     * Handles an exception, ultimately generating a Whoops error page.
340
     *
341
     * @param Throwable $exception
342
     *
343
     * @return string Output generated by handlers.
344
     */
345 7
    public function handleException($exception)
346
    {
347
        // Walk the registered handlers in the reverse order
348
        // they were registered, and pass off the exception
349 7
        $inspector = $this->getInspector($exception);
350
351
        // Capture output produced while handling the exception,
352
        // we might want to send it straight away to the client,
353
        // or return it silently.
354 7
        $this->system->startOutputBuffering();
355
356
        // Just in case there are no handlers:
357 7
        $handlerResponse = null;
358 7
        $handlerContentType = null;
359
360
        try {
361 7
            foreach (array_reverse($this->handlerStack) as $handler) {
362 7
                $handler->setRun($this);
363 7
                $handler->setInspector($inspector);
364 7
                $handler->setException($exception);
365
366
                // The HandlerInterface does not require an Exception passed to handle()
367
                // and neither of our bundled handlers use it.
368
                // However, 3rd party handlers may have already relied on this parameter,
369
                // and removing it would be possibly breaking for users.
370 7
                $handlerResponse = $handler->handle($exception);
371
372
                // Collect the content type for possible sending in the headers.
373 7
                $handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null;
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Whoops\Handler\HandlerInterface as the method contentType() does only exist in the following implementations of said interface: Whoops\Handler\JsonResponseHandler, Whoops\Handler\PlainTextHandler, Whoops\Handler\PrettyPageHandler, Whoops\Handler\XmlResponseHandler.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
374
375 7
                if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
376
                    // The Handler has handled the exception in some way, and
377
                    // wishes to quit execution (Handler::QUIT), or skip any
378
                    // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
379
                    // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
380 4
                    break;
381
                }
382 7
            }
383
384 7
            $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
385 7
        } finally {
386 7
            $output = $this->system->cleanOutputBuffer();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->system->cleanOutputBuffer(); of type string|false adds false to the return on line 418 which is incompatible with the return type declared by the interface Whoops\RunInterface::handleException of type string. It seems like you forgot to handle an error condition.
Loading history...
387 7
        }
388
389
        // If we're allowed to, send output generated by handlers directly
390
        // to the output, otherwise, and if the script doesn't quit, return
391
        // it so that it may be used by the caller
392 7
        if ($this->writeToOutput()) {
393
            // @todo Might be able to clean this up a bit better
394 6
            if ($willQuit) {
0 ignored issues
show
The variable $willQuit does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
395
                // Cleanup all other output buffers before sending our output:
396
                while ($this->system->getOutputBufferLevel() > 0) {
397
                    $this->system->endOutputBuffering();
398
                }
399
400
                // Send any headers if needed:
401
                if (Misc::canSendHeaders() && $handlerContentType) {
402
                    header("Content-Type: {$handlerContentType}");
403
                }
404
            }
405
406 6
            $this->writeToOutputNow($output);
0 ignored issues
show
It seems like $output defined by $this->system->cleanOutputBuffer() on line 386 can also be of type false; however, Whoops\Run::writeToOutputNow() 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...
407 6
        }
408
409 7
        if ($willQuit) {
410
            // HHVM fix for https://github.com/facebook/hhvm/issues/4055
411
            $this->system->flushOutputBuffer();
412
413
            $this->system->stopExecution(
414
                $this->sendExitCode()
0 ignored issues
show
It seems like $this->sendExitCode() targeting Whoops\Run::sendExitCode() can also be of type false; however, Whoops\Util\SystemFacade::stopExecution() does only seem to accept integer, did you maybe forget to handle an error condition?
Loading history...
415
            );
416
        }
417
418 7
        return $output;
419
    }
420
421
    /**
422
     * Converts generic PHP errors to \ErrorException instances, before passing them off to be handled.
423
     *
424
     * This method MUST be compatible with set_error_handler.
425
     *
426
     * @param int         $level
427
     * @param string      $message
428
     * @param string|null $file
429
     * @param int|null    $line
430
     *
431
     * @return bool
432
     *
433
     * @throws ErrorException
434
     */
435 5
    public function handleError($level, $message, $file = null, $line = null)
436
    {
437 5
        if ($level & $this->system->getErrorReportingLevel()) {
438 2
            foreach ($this->silencedPatterns as $entry) {
439
                $pathMatches = (bool) preg_match($entry["pattern"], $file);
440
                $levelMatches = $level & $entry["levels"];
441
                if ($pathMatches && $levelMatches) {
442
                    // Ignore the error, abort handling
443
                    // See https://github.com/filp/whoops/issues/418
444
                    return true;
445
                }
446 2
            }
447
448
            // XXX we pass $level for the "code" param only for BC reasons.
449
            // see https://github.com/filp/whoops/issues/267
450 2
            $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
451 2
            if ($this->canThrowExceptions) {
452 2
                throw $exception;
453
            } else {
454
                $this->handleException($exception);
0 ignored issues
show
$exception is of type object<Whoops\Exception\ErrorException>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
455
            }
456
            // Do not propagate errors which were already handled by Whoops.
457
            return true;
458
        }
459
460
        // Propagate error to the next handler, allows error_get_last() to
461
        // work on silenced errors.
462 3
        return false;
463
    }
464
465
    /**
466
     * Special case to deal with Fatal errors and the like.
467
     *
468
     * @return void
469
     */
470
    public function handleShutdown()
471
    {
472
        // If we reached this step, we are in shutdown handler.
473
        // An exception thrown in a shutdown handler will not be propagated
474
        // to the exception handler. Pass that information along.
475
        $this->canThrowExceptions = false;
476
477
        $error = $this->system->getLastError();
478
        if ($error && Misc::isLevelFatal($error['type'])) {
479
            // If there was a fatal error,
480
            // it was not handled in handleError yet.
481
            $this->allowQuit = false;
482
            $this->handleError(
483
                $error['type'],
484
                $error['message'],
485
                $error['file'],
486
                $error['line']
487
            );
488
        }
489
    }
490
491
    /**
492
     * @param Throwable $exception
493
     *
494
     * @return Inspector
495
     */
496 3
    private function getInspector($exception)
497
    {
498 3
        return new Inspector($exception);
499
    }
500
501
    /**
502
     * Resolves the giving handler.
503
     *
504
     * @param HandlerInterface $handler
505
     *
506
     * @return HandlerInterface
507
     *
508
     * @throws InvalidArgumentException
509
     */
510 6
    private function resolveHandler($handler)
511
    {
512 6
        if (is_callable($handler)) {
513
            $handler = new CallbackHandler($handler);
514
        }
515
516 6
        if (!$handler instanceof HandlerInterface) {
517
            throw new InvalidArgumentException(
518
                "Handler must be a callable, or instance of "
519
                . "Whoops\\Handler\\HandlerInterface"
520
            );
521
        }
522
523 6
        return $handler;
524
    }
525
526
    /**
527
     * Echo something to the browser.
528
     *
529
     * @param string $output
530
     *
531
     * @return Run
532
     */
533 3
    private function writeToOutputNow($output)
534
    {
535 3
        if ($this->sendHttpCode() && Misc::canSendHeaders()) {
536
            $this->system->setHttpResponseCode(
537
                $this->sendHttpCode()
0 ignored issues
show
It seems like $this->sendHttpCode() targeting Whoops\Run::sendHttpCode() can also be of type boolean; however, Whoops\Util\SystemFacade::setHttpResponseCode() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
538
            );
539
        }
540
541 3
        echo $output;
542
543 3
        return $this;
544
    }
545
}
546