Passed
Push — master ( a7be49...5129e4 )
by Maurício
08:55
created

libraries/classes/Config/ServerConfigChecks.php (2 issues)

1
<?php
2
/**
3
 * Server config checks management
4
 */
5
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin\Config;
9
10
use PhpMyAdmin\Core;
11
use PhpMyAdmin\Sanitize;
12
use PhpMyAdmin\Setup\Index as SetupIndex;
13
use PhpMyAdmin\Url;
14
use PhpMyAdmin\Util;
15
use function count;
16
use function function_exists;
17
use function htmlspecialchars;
18
use function implode;
19
use function ini_get;
20
use function preg_match;
21
use function sprintf;
22
use function strlen;
23
24
/**
25
 * Performs various compatibility, security and consistency checks on current config
26
 *
27
 * Outputs results to message list, must be called between SetupIndex::messagesBegin()
28
 * and SetupIndex::messagesEnd()
29
 */
30
class ServerConfigChecks
31
{
32
    /** @var ConfigFile configurations being checked */
33
    protected $cfg;
34
35
    /**
36
     * @param ConfigFile $cfg Configuration
37
     */
38 12
    public function __construct(ConfigFile $cfg)
39
    {
40 12
        $this->cfg = $cfg;
41 12
    }
42
43
    /**
44
     * Perform config checks
45
     *
46
     * @return void
47
     */
48 12
    public function performConfigChecks()
49
    {
50 12
        $blowfishSecret = $this->cfg->get('blowfish_secret');
51 12
        $blowfishSecretSet = false;
52 12
        $cookieAuthUsed = false;
53
54
        [$cookieAuthUsed, $blowfishSecret, $blowfishSecretSet]
55 12
            = $this->performConfigChecksServers(
56 12
                $cookieAuthUsed,
57 6
                $blowfishSecret,
0 ignored issues
show
It seems like $blowfishSecret can also be of type array; however, parameter $blowfishSecret of PhpMyAdmin\Config\Server...rmConfigChecksServers() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

57
                /** @scrutinizer ignore-type */ $blowfishSecret,
Loading history...
58 6
                $blowfishSecretSet
59
            );
60
61 12
        $this->performConfigChecksCookieAuthUsed(
62 12
            $cookieAuthUsed,
0 ignored issues
show
It seems like $cookieAuthUsed can also be of type integer; however, parameter $cookieAuthUsed of PhpMyAdmin\Config\Server...gChecksCookieAuthUsed() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

62
            /** @scrutinizer ignore-type */ $cookieAuthUsed,
Loading history...
63 6
            $blowfishSecretSet,
64 6
            $blowfishSecret
65
        );
66
67
        // $cfg['AllowArbitraryServer']
68
        // should be disabled
69 12
        if ($this->cfg->getValue('AllowArbitraryServer')) {
70 4
            $sAllowArbitraryServerWarn = sprintf(
71 4
                __(
72
                    'This %soption%s should be disabled as it allows attackers to '
73
                    . 'bruteforce login to any MySQL server. If you feel this is necessary, '
74
                    . 'use %srestrict login to MySQL server%s or %strusted proxies list%s. '
75
                    . 'However, IP-based protection with trusted proxies list may not be '
76
                    . 'reliable if your IP belongs to an ISP where thousands of users, '
77 4
                    . 'including you, are connected to.'
78
                ),
79 4
                '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
80 4
                '[/a]',
81 4
                '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
82 4
                '[/a]',
83 4
                '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
84 4
                '[/a]'
85
            );
86 4
            SetupIndex::messagesSet(
87 4
                'notice',
88 4
                'AllowArbitraryServer',
89 4
                Descriptions::get('AllowArbitraryServer'),
90 4
                Sanitize::sanitizeMessage($sAllowArbitraryServerWarn)
91
            );
92
        }
93
94 12
        $this->performConfigChecksLoginCookie();
95
96 12
        $sDirectoryNotice = __(
97
            'This value should be double checked to ensure that this directory is '
98
            . 'neither world accessible nor readable or writable by other users on '
99 12
            . 'your server.'
100
        );
101
102
        // $cfg['SaveDir']
103
        // should not be world-accessible
104 12
        if ($this->cfg->getValue('SaveDir') != '') {
105 4
            SetupIndex::messagesSet(
106 4
                'notice',
107 4
                'SaveDir',
108 4
                Descriptions::get('SaveDir'),
109 4
                Sanitize::sanitizeMessage($sDirectoryNotice)
110
            );
111
        }
112
113
        // $cfg['TempDir']
114
        // should not be world-accessible
115 12
        if ($this->cfg->getValue('TempDir') != '') {
116 8
            SetupIndex::messagesSet(
117 8
                'notice',
118 8
                'TempDir',
119 8
                Descriptions::get('TempDir'),
120 8
                Sanitize::sanitizeMessage($sDirectoryNotice)
121
            );
122
        }
123
124 12
        $this->performConfigChecksZips();
125 12
    }
126
127
    /**
128
     * Check config of servers
129
     *
130
     * @param bool   $cookieAuthUsed    Cookie auth is used
131
     * @param string $blowfishSecret    Blowfish secret
132
     * @param bool   $blowfishSecretSet Blowfish secret set
133
     *
134
     * @return array
135
     */
136 12
    protected function performConfigChecksServers(
137
        $cookieAuthUsed,
138
        $blowfishSecret,
139
        $blowfishSecretSet
140
    ) {
141 12
        $serverCnt = $this->cfg->getServerCount();
142 12
        for ($i = 1; $i <= $serverCnt; $i++) {
143
            $cookieAuthServer
144 12
                = ($this->cfg->getValue('Servers/' . $i . '/auth_type') == 'cookie');
145 12
            $cookieAuthUsed |= $cookieAuthServer;
146 12
            $serverName = $this->performConfigChecksServersGetServerName(
147 12
                $this->cfg->getServerName($i),
148 6
                $i
149
            );
150 12
            $serverName = htmlspecialchars($serverName);
151
152
            [$blowfishSecret, $blowfishSecretSet]
153 12
                = $this->performConfigChecksServersSetBlowfishSecret(
154 12
                    $blowfishSecret,
155 6
                    $cookieAuthServer,
156 6
                    $blowfishSecretSet
157
                );
158
159
            // $cfg['Servers'][$i]['ssl']
160
            // should be enabled if possible
161 12
            if (! $this->cfg->getValue('Servers/' . $i . '/ssl')) {
162 8
                $title = Descriptions::get('Servers/1/ssl') . ' (' . $serverName . ')';
163 8
                SetupIndex::messagesSet(
164 8
                    'notice',
165 8
                    'Servers/' . $i . '/ssl',
166 6
                    $title,
167 8
                    __(
168
                        'You should use SSL connections if your database server '
169 8
                        . 'supports it.'
170
                    )
171
                );
172
            }
173 12
            $sSecurityInfoMsg = Sanitize::sanitizeMessage(sprintf(
174 12
                __(
175
                    'If you feel this is necessary, use additional protection settings - '
176
                    . '%1$shost authentication%2$s settings and %3$strusted proxies list%4%s. '
177
                    . 'However, IP-based protection may not be reliable if your IP belongs '
178 12
                    . 'to an ISP where thousands of users, including you, are connected to.'
179
                ),
180 12
                '[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server_config]',
181 12
                '[/a]',
182 12
                '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
183 12
                '[/a]'
184
            ));
185
186
            // $cfg['Servers'][$i]['auth_type']
187
            // warn about full user credentials if 'auth_type' is 'config'
188 12
            if ($this->cfg->getValue('Servers/' . $i . '/auth_type') == 'config'
189 12
                && $this->cfg->getValue('Servers/' . $i . '/user') != ''
190 12
                && $this->cfg->getValue('Servers/' . $i . '/password') != ''
191
            ) {
192 4
                $title = Descriptions::get('Servers/1/auth_type')
193 4
                    . ' (' . $serverName . ')';
194 4
                SetupIndex::messagesSet(
195 4
                    'notice',
196 4
                    'Servers/' . $i . '/auth_type',
197 3
                    $title,
198 4
                    Sanitize::sanitizeMessage(sprintf(
199 4
                        __(
200
                            'You set the [kbd]config[/kbd] authentication type and included '
201
                            . 'username and password for auto-login, which is not a desirable '
202
                            . 'option for live hosts. Anyone who knows or guesses your phpMyAdmin '
203
                            . 'URL can directly access your phpMyAdmin panel. Set %1$sauthentication '
204 4
                            . 'type%2$s to [kbd]cookie[/kbd] or [kbd]http[/kbd].'
205
                        ),
206 4
                        '[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server]',
207 4
                        '[/a]'
208
                    ))
209 4
                    . ' ' . $sSecurityInfoMsg
210
                );
211
            }
212
213
            // $cfg['Servers'][$i]['AllowRoot']
214
            // $cfg['Servers'][$i]['AllowNoPassword']
215
            // serious security flaw
216 12
            if (! $this->cfg->getValue('Servers/' . $i . '/AllowRoot')
217 12
                || ! $this->cfg->getValue('Servers/' . $i . '/AllowNoPassword')
218
            ) {
219 8
                continue;
220
            }
221
222 4
            $title = Descriptions::get('Servers/1/AllowNoPassword')
223 4
                . ' (' . $serverName . ')';
224 4
            SetupIndex::messagesSet(
225 4
                'notice',
226 4
                'Servers/' . $i . '/AllowNoPassword',
227 3
                $title,
228 4
                __('You allow for connecting to the server without a password.')
229 4
                . ' ' . $sSecurityInfoMsg
230
            );
231
        }
232
233
        return [
234 12
            $cookieAuthUsed,
235 12
            $blowfishSecret,
236 12
            $blowfishSecretSet,
237
        ];
238
    }
239
240
    /**
241
     * Set blowfish secret
242
     *
243
     * @param string|null $blowfishSecret    Blowfish secret
244
     * @param bool        $cookieAuthServer  Cookie auth is used
245
     * @param bool        $blowfishSecretSet Blowfish secret set
246
     *
247
     * @return array
248
     */
249 12
    protected function performConfigChecksServersSetBlowfishSecret(
250
        $blowfishSecret,
251
        $cookieAuthServer,
252
        $blowfishSecretSet
253
    ): array {
254 12
        if ($cookieAuthServer && $blowfishSecret === null) {
255 4
            $blowfishSecretSet = true;
256 4
            $this->cfg->set('blowfish_secret', Util::generateRandom(32));
257
        }
258
259
        return [
260 12
            $blowfishSecret,
261 12
            $blowfishSecretSet,
262
        ];
263
    }
264
265
    /**
266
     * Define server name
267
     *
268
     * @param string $serverName Server name
269
     * @param int    $serverId   Server id
270
     *
271
     * @return string Server name
272
     */
273 12
    protected function performConfigChecksServersGetServerName(
274
        $serverName,
275
        $serverId
276
    ) {
277 12
        if ($serverName == 'localhost') {
278 12
            return $serverName . ' [' . $serverId . ']';
279
        }
280
281
        return $serverName;
282
    }
283
284
    /**
285
     * Perform config checks for zip part.
286
     *
287
     * @return void
288
     */
289 12
    protected function performConfigChecksZips()
290
    {
291 12
        $this->performConfigChecksServerGZipdump();
292 12
        $this->performConfigChecksServerBZipdump();
293 12
        $this->performConfigChecksServersZipdump();
294 12
    }
295
296
    /**
297
     * Perform config checks for zip part.
298
     *
299
     * @return void
300
     */
301 12
    protected function performConfigChecksServersZipdump()
302
    {
303
        // $cfg['ZipDump']
304
        // requires zip_open in import
305 12
        if ($this->cfg->getValue('ZipDump') && ! $this->functionExists('zip_open')) {
306 4
            SetupIndex::messagesSet(
307 4
                'error',
308 4
                'ZipDump_import',
309 4
                Descriptions::get('ZipDump'),
310 4
                Sanitize::sanitizeMessage(sprintf(
311 4
                    __(
312
                        '%sZip decompression%s requires functions (%s) which are unavailable '
313 4
                        . 'on this system.'
314
                    ),
315 4
                    '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
316 4
                    '[/a]',
317 4
                    'zip_open'
318
                ))
319
            );
320
        }
321
322
        // $cfg['ZipDump']
323
        // requires gzcompress in export
324 12
        if (! $this->cfg->getValue('ZipDump') || $this->functionExists('gzcompress')) {
325 8
            return;
326
        }
327
328 4
        SetupIndex::messagesSet(
329 4
            'error',
330 4
            'ZipDump_export',
331 4
            Descriptions::get('ZipDump'),
332 4
            Sanitize::sanitizeMessage(sprintf(
333 4
                __(
334
                    '%sZip compression%s requires functions (%s) which are unavailable on '
335 4
                    . 'this system.'
336
                ),
337 4
                '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
338 4
                '[/a]',
339 4
                'gzcompress'
340
            ))
341
        );
342 4
    }
343
344
    /**
345
     * Check config of servers
346
     *
347
     * @param bool   $cookieAuthUsed    Cookie auth is used
348
     * @param bool   $blowfishSecretSet Blowfish secret set
349
     * @param string $blowfishSecret    Blowfish secret
350
     *
351
     * @return void
352
     */
353 12
    protected function performConfigChecksCookieAuthUsed(
354
        $cookieAuthUsed,
355
        $blowfishSecretSet,
356
        $blowfishSecret
357
    ) {
358
        // $cfg['blowfish_secret']
359
        // it's required for 'cookie' authentication
360 12
        if (! $cookieAuthUsed) {
361 4
            return;
362
        }
363
364 8
        if ($blowfishSecretSet) {
365
            // 'cookie' auth used, blowfish_secret was generated
366 4
            SetupIndex::messagesSet(
367 4
                'notice',
368 4
                'blowfish_secret_created',
369 4
                Descriptions::get('blowfish_secret'),
370 4
                Sanitize::sanitizeMessage(__(
371
                    'You didn\'t have blowfish secret set and have enabled '
372
                    . '[kbd]cookie[/kbd] authentication, so a key was automatically '
373
                    . 'generated for you. It is used to encrypt cookies; you don\'t need to '
374 4
                    . 'remember it.'
375
                ))
376
            );
377
        } else {
378 4
            $blowfishWarnings = [];
379
            // check length
380 4
            if (strlen($blowfishSecret) < 32) {
381
                // too short key
382 4
                $blowfishWarnings[] = __(
383 4
                    'Key is too short, it should have at least 32 characters.'
384
                );
385
            }
386
            // check used characters
387 4
            $hasDigits = (bool) preg_match('/\d/', $blowfishSecret);
388 4
            $hasChars = (bool) preg_match('/\S/', $blowfishSecret);
389 4
            $hasNonword = (bool) preg_match('/\W/', $blowfishSecret);
390 4
            if (! $hasDigits || ! $hasChars || ! $hasNonword) {
391 4
                $blowfishWarnings[] = Sanitize::sanitizeMessage(
392 4
                    __(
393
                        'Key should contain letters, numbers [em]and[/em] '
394 4
                        . 'special characters.'
395
                    )
396
                );
397
            }
398 4
            if (! empty($blowfishWarnings)) {
399 4
                SetupIndex::messagesSet(
400 4
                    'error',
401 4
                    'blowfish_warnings' . count($blowfishWarnings),
402 4
                    Descriptions::get('blowfish_secret'),
403 4
                    implode('<br>', $blowfishWarnings)
404
                );
405
            }
406
        }
407 8
    }
408
409
    /**
410
     * Check configuration for login cookie
411
     *
412
     * @return void
413
     */
414 12
    protected function performConfigChecksLoginCookie()
415
    {
416
        // $cfg['LoginCookieValidity']
417
        // value greater than session.gc_maxlifetime will cause
418
        // random session invalidation after that time
419 12
        $loginCookieValidity = $this->cfg->getValue('LoginCookieValidity');
420 12
        if ($loginCookieValidity > ini_get('session.gc_maxlifetime')
421
        ) {
422 4
            SetupIndex::messagesSet(
423 4
                'error',
424 4
                'LoginCookieValidity',
425 4
                Descriptions::get('LoginCookieValidity'),
426 4
                Sanitize::sanitizeMessage(sprintf(
427 4
                    __(
428
                        '%1$sLogin cookie validity%2$s greater than %3$ssession.gc_maxlifetime%4$s may '
429
                        . 'cause random session invalidation (currently session.gc_maxlifetime '
430 4
                        . 'is %5$d).'
431
                    ),
432 4
                    '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
433 4
                    '[/a]',
434 4
                    '[a@' . Core::getPHPDocLink('session.configuration.php#ini.session.gc-maxlifetime') . ']',
435 4
                    '[/a]',
436 4
                    ini_get('session.gc_maxlifetime')
437
                ))
438
            );
439
        }
440
441
        // $cfg['LoginCookieValidity']
442
        // should be at most 1800 (30 min)
443 12
        if ($loginCookieValidity > 1800) {
444 4
            SetupIndex::messagesSet(
445 4
                'notice',
446 4
                'LoginCookieValidity',
447 4
                Descriptions::get('LoginCookieValidity'),
448 4
                Sanitize::sanitizeMessage(sprintf(
449 4
                    __(
450
                        '%sLogin cookie validity%s should be set to 1800 seconds (30 minutes) '
451
                        . 'at most. Values larger than 1800 may pose a security risk such as '
452 4
                        . 'impersonation.'
453
                    ),
454 4
                    '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
455 4
                    '[/a]'
456
                ))
457
            );
458
        }
459
460
        // $cfg['LoginCookieValidity']
461
        // $cfg['LoginCookieStore']
462
        // LoginCookieValidity must be less or equal to LoginCookieStore
463 12
        if (($this->cfg->getValue('LoginCookieStore') == 0)
464 12
            || ($loginCookieValidity <= $this->cfg->getValue('LoginCookieStore'))
465
        ) {
466 8
            return;
467
        }
468
469 4
        SetupIndex::messagesSet(
470 4
            'error',
471 4
            'LoginCookieValidity',
472 4
            Descriptions::get('LoginCookieValidity'),
473 4
            Sanitize::sanitizeMessage(sprintf(
474 4
                __(
475
                    'If using [kbd]cookie[/kbd] authentication and %sLogin cookie store%s '
476
                    . 'is not 0, %sLogin cookie validity%s must be set to a value less or '
477 4
                    . 'equal to it.'
478
                ),
479 4
                '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
480 4
                '[/a]',
481 4
                '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]',
482 4
                '[/a]'
483
            ))
484
        );
485 4
    }
486
487
    /**
488
     * Check GZipDump configuration
489
     *
490
     * @return void
491
     */
492 12
    protected function performConfigChecksServerBZipdump()
493
    {
494
        // $cfg['BZipDump']
495
        // requires bzip2 functions
496 12
        if (! $this->cfg->getValue('BZipDump')
497 12
            || ($this->functionExists('bzopen') && $this->functionExists('bzcompress'))
498
        ) {
499 8
            return;
500
        }
501
502 4
        $functions = $this->functionExists('bzopen')
503
            ? '' :
504 4
            'bzopen';
505 4
        $functions .= $this->functionExists('bzcompress')
506
            ? ''
507 4
            : ($functions ? ', ' : '') . 'bzcompress';
508 4
        SetupIndex::messagesSet(
509 4
            'error',
510 4
            'BZipDump',
511 4
            Descriptions::get('BZipDump'),
512 4
            Sanitize::sanitizeMessage(
513 4
                sprintf(
514 4
                    __(
515
                        '%1$sBzip2 compression and decompression%2$s requires functions (%3$s) which '
516 4
                         . 'are unavailable on this system.'
517
                    ),
518 4
                    '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
519 4
                    '[/a]',
520 4
                    $functions
521
                )
522
            )
523
        );
524 4
    }
525
526
    /**
527
     * Check GZipDump configuration
528
     *
529
     * @return void
530
     */
531 12
    protected function performConfigChecksServerGZipdump()
532
    {
533
        // $cfg['GZipDump']
534
        // requires zlib functions
535 12
        if (! $this->cfg->getValue('GZipDump')
536 12
            || ($this->functionExists('gzopen') && $this->functionExists('gzencode'))
537
        ) {
538 8
            return;
539
        }
540
541 4
        SetupIndex::messagesSet(
542 4
            'error',
543 4
            'GZipDump',
544 4
            Descriptions::get('GZipDump'),
545 4
            Sanitize::sanitizeMessage(sprintf(
546 4
                __(
547
                    '%1$sGZip compression and decompression%2$s requires functions (%3$s) which '
548 4
                    . 'are unavailable on this system.'
549
                ),
550 4
                '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]',
551 4
                '[/a]',
552 4
                'gzencode'
553
            ))
554
        );
555 4
    }
556
557
    /**
558
     * Wrapper around function_exists to allow mock in test
559
     *
560
     * @param string $name Function name
561
     *
562
     * @return bool
563
     */
564 4
    protected function functionExists($name)
565
    {
566 4
        return function_exists($name);
567
    }
568
}
569