Issues (1885)

unit/Plugins/Auth/AuthenticationCookieTest.php (6 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Tests\Plugins\Auth;
6
7
use Fig\Http\Message\StatusCodeInterface;
8
use PhpMyAdmin\Config;
9
use PhpMyAdmin\Current;
10
use PhpMyAdmin\DatabaseInterface;
11
use PhpMyAdmin\Exceptions\AuthenticationFailure;
12
use PhpMyAdmin\Exceptions\ExitException;
13
use PhpMyAdmin\Plugins\Auth\AuthenticationCookie;
14
use PhpMyAdmin\ResponseRenderer;
15
use PhpMyAdmin\Tests\AbstractTestCase;
16
use PhpMyAdmin\Tests\Stubs\ResponseRenderer as ResponseRendererStub;
17
use PHPUnit\Framework\Attributes\CoversClass;
18
use PHPUnit\Framework\Attributes\DataProvider;
19
use PHPUnit\Framework\Attributes\Medium;
20
use ReflectionException;
21
use ReflectionMethod;
22
use ReflectionProperty;
23
use Throwable;
24
25
use function base64_decode;
26
use function base64_encode;
27
use function is_readable;
28
use function json_decode;
29
use function json_encode;
30
use function mb_strlen;
31
use function ob_get_clean;
32
use function ob_start;
33
use function random_bytes;
34
use function str_repeat;
35
use function str_shuffle;
36
use function time;
37
38
use const SODIUM_CRYPTO_SECRETBOX_KEYBYTES;
39
40
#[CoversClass(AuthenticationCookie::class)]
41
#[Medium]
42
class AuthenticationCookieTest extends AbstractTestCase
43
{
44
    protected AuthenticationCookie $object;
45
46
    /**
47
     * Configures global environment.
48
     */
49
    protected function setUp(): void
50
    {
51
        parent::setUp();
52
53
        $this->setLanguage();
54
55
        $this->setGlobalConfig();
56
57
        DatabaseInterface::$instance = $this->createDatabaseInterface();
58
        Current::$database = 'db';
59
        Current::$table = 'table';
60
        $_POST['pma_password'] = '';
61
        $this->object = new AuthenticationCookie();
62
        $_SERVER['PHP_SELF'] = '/phpmyadmin/index.php';
63
        Config::getInstance()->selectedServer['DisableIS'] = false;
64
        $GLOBALS['conn_error'] = null;
65
    }
66
67
    /**
68
     * tearDown for test cases
69
     */
70
    protected function tearDown(): void
71
    {
72
        parent::tearDown();
73
74
        unset($this->object);
75
    }
76
77
    public function testAuthErrorAJAX(): void
78
    {
79
        $GLOBALS['conn_error'] = true;
80
81
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
82
        $responseRenderer = ResponseRenderer::getInstance();
83
        $responseRenderer->setAjax(true);
84
85
        $response = $this->object->showLoginForm();
86
87
        self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
88
        $body = (string) $response->getBody();
89
        self::assertJson($body);
90
        $json = json_decode($body, true);
91
        self::assertIsArray($json);
92
        self::assertArrayHasKey('success', $json);
93
        self::assertFalse($json['success']);
94
        self::assertArrayHasKey('redirect_flag', $json);
95
        self::assertSame('1', $json['redirect_flag']);
96
    }
97
98
    public function testAuthError(): void
99
    {
100
        $_REQUEST = [];
101
102
        $_REQUEST['old_usr'] = '';
103
        $config = Config::getInstance();
104
        $config->settings['LoginCookieRecall'] = true;
105
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
106
        $this->object->user = 'pmauser';
107
        $GLOBALS['pma_auth_server'] = 'localhost';
108
109
        $GLOBALS['conn_error'] = true;
110
        $config->settings['Lang'] = 'en';
111
        $config->settings['AllowArbitraryServer'] = true;
112
        $config->settings['CaptchaApi'] = '';
113
        $config->settings['CaptchaRequestParam'] = '';
114
        $config->settings['CaptchaResponseParam'] = '';
115
        $config->settings['CaptchaLoginPrivateKey'] = '';
116
        $config->settings['CaptchaLoginPublicKey'] = '';
117
        Current::$database = 'testDb';
118
        Current::$table = 'testTable';
119
        $config->settings['Servers'] = [1, 2];
120
121
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
122
123
        $response = $this->object->showLoginForm();
124
125
        $result = (string) $response->getBody();
126
127
        self::assertStringContainsString(' id="imLogo"', $result);
128
129
        self::assertStringContainsString('<div class="alert alert-danger" role="alert">', $result);
130
131
        self::assertStringContainsString(
132
            '<form method="post" id="login_form" action="index.php?route=/" name="login_form" ' .
133
            'class="disableAjax hide js-show">',
134
            $result,
135
        );
136
137
        self::assertStringContainsString(
138
            '<input type="text" name="pma_servername" id="serverNameInput" value="localhost"',
139
            $result,
140
        );
141
142
        self::assertStringContainsString(
143
            '<input type="text" name="pma_username" id="input_username" ' .
144
            'value="pmauser" class="form-control" autocomplete="username" spellcheck="false" autofocus>',
145
            $result,
146
        );
147
148
        self::assertStringContainsString(
149
            '<input type="password" name="pma_password" id="input_password" ' .
150
            'value="" class="form-control" autocomplete="current-password" spellcheck="false">',
151
            $result,
152
        );
153
154
        self::assertStringContainsString(
155
            '<select name="server" id="select_server" class="form-select" ' .
156
            'onchange="document.forms[\'login_form\'].' .
157
            'elements[\'pma_servername\'].value = \'\'">',
158
            $result,
159
        );
160
161
        self::assertStringContainsString('<input type="hidden" name="db" value="testDb">', $result);
162
163
        self::assertStringContainsString('<input type="hidden" name="table" value="testTable">', $result);
164
    }
165
166
    public function testAuthCaptcha(): void
167
    {
168
        $_REQUEST['old_usr'] = '';
169
        $config = Config::getInstance();
170
        $config->settings['LoginCookieRecall'] = false;
171
172
        $config->settings['Lang'] = '';
173
        $config->settings['AllowArbitraryServer'] = false;
174
        $config->settings['Servers'] = [1];
175
        $config->settings['CaptchaApi'] = 'https://www.google.com/recaptcha/api.js';
176
        $config->settings['CaptchaRequestParam'] = 'g-recaptcha';
177
        $config->settings['CaptchaResponseParam'] = 'g-recaptcha-response';
178
        $config->settings['CaptchaLoginPrivateKey'] = 'testprivkey';
179
        $config->settings['CaptchaLoginPublicKey'] = 'testpubkey';
180
        Current::$server = 2;
181
182
        $responseStub = new ResponseRendererStub();
183
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
184
185
        $response = $this->object->showLoginForm();
186
187
        $result = (string) $response->getBody();
188
189
        self::assertStringContainsString('id="imLogo"', $result);
190
191
        // Check for language selection if locales are there
192
        $loc = LOCALE_PATH . '/cs/LC_MESSAGES/phpmyadmin.mo';
193
        if (is_readable($loc)) {
194
            self::assertStringContainsString(
195
                '<select name="lang" class="form-select autosubmit" lang="en" dir="ltr"'
196
                . ' id="languageSelect" aria-labelledby="languageSelectLabel">',
197
                $result,
198
            );
199
        }
200
201
        self::assertStringContainsString(
202
            '<form method="post" id="login_form" action="index.php?route=/" name="login_form"' .
203
            ' class="disableAjax hide js-show" autocomplete="off">',
204
            $result,
205
        );
206
207
        self::assertStringContainsString('<input type="hidden" name="server" value="2">', $result);
208
209
        self::assertStringContainsString(
210
            '<script src="https://www.google.com/recaptcha/api.js?hl=en" async defer></script>',
211
            $result,
212
        );
213
214
        self::assertStringContainsString(
215
            '<input class="btn btn-primary g-recaptcha" data-sitekey="testpubkey"'
216
            . ' data-callback="recaptchaCallback" value="Log in" type="submit" id="input_go">',
217
            $result,
218
        );
219
    }
220
221
    public function testAuthCaptchaCheckbox(): void
222
    {
223
        $_REQUEST['old_usr'] = '';
224
        $config = Config::getInstance();
225
        $config->settings['LoginCookieRecall'] = false;
226
227
        $config->settings['Lang'] = '';
228
        $config->settings['AllowArbitraryServer'] = false;
229
        $config->settings['Servers'] = [1];
230
        $config->settings['CaptchaApi'] = 'https://www.google.com/recaptcha/api.js';
231
        $config->settings['CaptchaRequestParam'] = 'g-recaptcha';
232
        $config->settings['CaptchaResponseParam'] = 'g-recaptcha-response';
233
        $config->settings['CaptchaLoginPrivateKey'] = 'testprivkey';
234
        $config->settings['CaptchaLoginPublicKey'] = 'testpubkey';
235
        $config->settings['CaptchaMethod'] = 'checkbox';
236
        Current::$server = 2;
237
238
        $responseStub = new ResponseRendererStub();
239
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
240
241
        $response = $this->object->showLoginForm();
242
243
        $result = (string) $response->getBody();
244
245
        self::assertStringContainsString('id="imLogo"', $result);
246
247
        // Check for language selection if locales are there
248
        $loc = LOCALE_PATH . '/cs/LC_MESSAGES/phpmyadmin.mo';
249
        if (is_readable($loc)) {
250
            self::assertStringContainsString(
251
                '<select name="lang" class="form-select autosubmit" lang="en" dir="ltr"'
252
                . ' id="languageSelect" aria-labelledby="languageSelectLabel">',
253
                $result,
254
            );
255
        }
256
257
        self::assertStringContainsString(
258
            '<form method="post" id="login_form" action="index.php?route=/" name="login_form"' .
259
            ' class="disableAjax hide js-show" autocomplete="off">',
260
            $result,
261
        );
262
263
        self::assertStringContainsString('<input type="hidden" name="server" value="2">', $result);
264
265
        self::assertStringContainsString(
266
            '<script src="https://www.google.com/recaptcha/api.js?hl=en" async defer></script>',
267
            $result,
268
        );
269
270
        self::assertStringContainsString('<div class="g-recaptcha" data-sitekey="testpubkey"></div>', $result);
271
272
        self::assertStringContainsString(
273
            '<input class="btn btn-primary" value="Log in" type="submit" id="input_go">',
274
            $result,
275
        );
276
    }
277
278
    public function testAuthHeader(): void
279
    {
280
        $config = Config::getInstance();
281
        $config->settings['LoginCookieDeleteAll'] = false;
282
        $config->settings['Servers'] = [1];
283
284
        $responseStub = new ResponseRendererStub();
285
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
286
287
        $config->selectedServer['LogoutURL'] = 'https://example.com/logout';
288
        $config->selectedServer['auth_type'] = 'cookie';
289
290
        $this->object->logOut();
291
292
        $response = $responseStub->getResponse();
293
        self::assertSame(['https://example.com/logout'], $response->getHeader('Location'));
294
        self::assertSame(302, $response->getStatusCode());
295
    }
296
297
    public function testAuthHeaderPartial(): void
298
    {
299
        $config = Config::getInstance();
300
        $config->set('is_https', false);
301
        $config->settings['LoginCookieDeleteAll'] = false;
302
        $config->settings['Servers'] = [1, 2, 3];
303
        $config->selectedServer['LogoutURL'] = 'https://example.com/logout';
304
        $config->selectedServer['auth_type'] = 'cookie';
305
306
        $_COOKIE['pmaAuth-2'] = '';
307
308
        $responseStub = new ResponseRendererStub();
309
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
310
311
        $this->object->logOut();
312
313
        $response = $responseStub->getResponse();
314
        self::assertSame(['/phpmyadmin/index.php?route=/&server=2&lang=en'], $response->getHeader('Location'));
315
        self::assertSame(302, $response->getStatusCode());
316
    }
317
318
    public function testAuthCheckCaptcha(): void
319
    {
320
        $config = Config::getInstance();
321
        $config->settings['CaptchaApi'] = 'https://www.google.com/recaptcha/api.js';
322
        $config->settings['CaptchaRequestParam'] = 'g-recaptcha';
323
        $config->settings['CaptchaResponseParam'] = 'g-recaptcha-response';
324
        $config->settings['CaptchaLoginPrivateKey'] = 'testprivkey';
325
        $config->settings['CaptchaLoginPublicKey'] = 'testpubkey';
326
        $_POST['g-recaptcha-response'] = '';
327
        $_POST['pma_username'] = 'testPMAUser';
328
329
        self::assertFalse(
330
            $this->object->readCredentials(),
331
        );
332
333
        self::assertSame('Missing Captcha verification, maybe it has been blocked by adblock?', $GLOBALS['conn_error']);
334
    }
335
336
    public function testLogoutDelete(): void
337
    {
338
        $responseStub = new ResponseRendererStub();
339
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
340
341
        $config = Config::getInstance();
342
        $config->settings['CaptchaApi'] = '';
343
        $config->settings['CaptchaRequestParam'] = '';
344
        $config->settings['CaptchaResponseParam'] = '';
345
        $config->settings['CaptchaLoginPrivateKey'] = '';
346
        $config->settings['CaptchaLoginPublicKey'] = '';
347
        $config->settings['LoginCookieDeleteAll'] = true;
348
        $config->set('PmaAbsoluteUri', '');
349
        $config->set('is_https', false);
350
        $config->settings['Servers'] = [1];
351
352
        $_COOKIE['pmaAuth-0'] = 'test';
353
354
        $this->object->logOut();
355
356
        $response = $responseStub->getResponse();
357
        self::assertSame(['/phpmyadmin/index.php?route=/'], $response->getHeader('Location'));
358
        self::assertSame(302, $response->getStatusCode());
359
360
        self::assertArrayNotHasKey('pmaAuth-0', $_COOKIE);
361
    }
362
363
    public function testLogout(): void
364
    {
365
        $responseStub = new ResponseRendererStub();
366
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
367
368
        $config = Config::getInstance();
369
        $config->settings['CaptchaApi'] = '';
370
        $config->settings['CaptchaRequestParam'] = '';
371
        $config->settings['CaptchaResponseParam'] = '';
372
        $config->settings['CaptchaLoginPrivateKey'] = '';
373
        $config->settings['CaptchaLoginPublicKey'] = '';
374
        $config->settings['LoginCookieDeleteAll'] = false;
375
        $config->set('PmaAbsoluteUri', '');
376
        $config->set('is_https', false);
377
        $config->settings['Servers'] = [1];
378
        $config->selectedServer = ['auth_type' => 'cookie'];
379
380
        $_COOKIE['pmaAuth-1'] = 'test';
381
382
        $this->object->logOut();
383
384
        $response = $responseStub->getResponse();
385
        self::assertSame(['/phpmyadmin/index.php?route=/'], $response->getHeader('Location'));
386
        self::assertSame(302, $response->getStatusCode());
387
388
        self::assertArrayNotHasKey('pmaAuth-1', $_COOKIE);
389
    }
390
391
    public function testAuthCheckArbitrary(): void
392
    {
393
        $config = Config::getInstance();
394
        $config->settings['CaptchaApi'] = '';
395
        $config->settings['CaptchaRequestParam'] = '';
396
        $config->settings['CaptchaResponseParam'] = '';
397
        $config->settings['CaptchaLoginPrivateKey'] = '';
398
        $config->settings['CaptchaLoginPublicKey'] = '';
399
        $_REQUEST['old_usr'] = '';
400
        $_POST['pma_username'] = 'testPMAUser';
401
        $_REQUEST['pma_servername'] = 'testPMAServer';
402
        $_POST['pma_password'] = 'testPMAPSWD';
403
        $config->settings['AllowArbitraryServer'] = true;
404
405
        self::assertTrue(
406
            $this->object->readCredentials(),
407
        );
408
409
        self::assertSame('testPMAUser', $this->object->user);
410
411
        self::assertSame('testPMAPSWD', $this->object->password);
412
413
        self::assertSame('testPMAServer', $GLOBALS['pma_auth_server']);
414
415
        self::assertArrayNotHasKey('pmaAuth-1', $_COOKIE);
416
    }
417
418
    public function testAuthCheckInvalidCookie(): void
419
    {
420
        Config::getInstance()->settings['AllowArbitraryServer'] = true;
421
        $_REQUEST['pma_servername'] = 'testPMAServer';
422
        $_POST['pma_password'] = 'testPMAPSWD';
423
        $_POST['pma_username'] = '';
424
        $_COOKIE['pmaUser-1'] = '';
425
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
426
427
        self::assertFalse(
428
            $this->object->readCredentials(),
429
        );
430
    }
431
432
    public function testAuthCheckExpires(): void
433
    {
434
        $_COOKIE['pmaServer-1'] = 'pmaServ1';
435
        $_COOKIE['pmaUser-1'] = 'pmaUser1';
436
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
437
        $_COOKIE['pmaAuth-1'] = '';
438
        $config = Config::getInstance();
439
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
440
        $_SESSION['last_access_time'] = time() - 1000;
441
        $config->settings['LoginCookieValidity'] = 1440;
442
443
        self::assertFalse(
444
            $this->object->readCredentials(),
445
        );
446
    }
447
448
    public function testAuthCheckDecryptUser(): void
449
    {
450
        $_REQUEST['old_usr'] = '';
451
        $_POST['pma_username'] = '';
452
        $_COOKIE['pmaServer-1'] = 'pmaServ1';
453
        $_COOKIE['pmaUser-1'] = 'pmaUser1';
454
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
455
        $config = Config::getInstance();
456
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
457
        $_SESSION['last_access_time'] = '';
458
        $config->settings['CaptchaApi'] = '';
459
        $config->settings['CaptchaRequestParam'] = '';
460
        $config->settings['CaptchaResponseParam'] = '';
461
        $config->settings['CaptchaLoginPrivateKey'] = '';
462
        $config->settings['CaptchaLoginPublicKey'] = '';
463
        $config->set('is_https', false);
464
465
        // mock for blowfish function
466
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
467
            ->disableOriginalConstructor()
468
            ->onlyMethods(['cookieDecrypt'])
469
            ->getMock();
470
471
        $this->object->expects(self::once())
472
            ->method('cookieDecrypt')
473
            ->willReturn('testBF');
474
475
        self::assertFalse(
476
            $this->object->readCredentials(),
477
        );
478
479
        self::assertSame('testBF', $this->object->user);
480
    }
481
482
    public function testAuthCheckDecryptPassword(): void
483
    {
484
        $_REQUEST['old_usr'] = '';
485
        $_POST['pma_username'] = '';
486
        $_COOKIE['pmaServer-1'] = 'pmaServ1';
487
        $_COOKIE['pmaUser-1'] = 'pmaUser1';
488
        $_COOKIE['pmaAuth-1'] = 'pmaAuth1';
489
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
490
        $config = Config::getInstance();
491
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
492
        $config->settings['CaptchaApi'] = '';
493
        $config->settings['CaptchaRequestParam'] = '';
494
        $config->settings['CaptchaResponseParam'] = '';
495
        $config->settings['CaptchaLoginPrivateKey'] = '';
496
        $config->settings['CaptchaLoginPublicKey'] = '';
497
        $_SESSION['browser_access_time']['default'] = time() - 1000;
498
        $config->settings['LoginCookieValidity'] = 1440;
499
        $config->set('is_https', false);
500
501
        // mock for blowfish function
502
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
503
            ->disableOriginalConstructor()
504
            ->onlyMethods(['cookieDecrypt'])
505
            ->getMock();
506
507
        $this->object->expects(self::exactly(2))
508
            ->method('cookieDecrypt')
509
            ->willReturn('{"password":""}');
510
511
        self::assertTrue(
512
            $this->object->readCredentials(),
513
        );
514
515
        self::assertTrue($GLOBALS['from_cookie']);
516
517
        self::assertSame('', $this->object->password);
518
    }
519
520
    public function testAuthCheckAuthFails(): void
521
    {
522
        $_REQUEST['old_usr'] = '';
523
        $_POST['pma_username'] = '';
524
        $_COOKIE['pmaServer-1'] = 'pmaServ1';
525
        $_COOKIE['pmaUser-1'] = 'pmaUser1';
526
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
527
        $config = Config::getInstance();
528
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
529
        $_SESSION['last_access_time'] = 1;
530
        $config->settings['CaptchaApi'] = '';
531
        $config->settings['CaptchaRequestParam'] = '';
532
        $config->settings['CaptchaResponseParam'] = '';
533
        $config->settings['CaptchaLoginPrivateKey'] = '';
534
        $config->settings['CaptchaLoginPublicKey'] = '';
535
        $config->settings['LoginCookieValidity'] = 0;
536
        $_SESSION['browser_access_time']['default'] = -1;
537
        $config->set('is_https', false);
538
539
        // mock for blowfish function
540
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
541
            ->disableOriginalConstructor()
542
            ->onlyMethods(['cookieDecrypt'])
543
            ->getMock();
544
545
        $this->object->expects(self::once())
546
            ->method('cookieDecrypt')
547
            ->willReturn('testBF');
548
549
        $this->expectExceptionObject(AuthenticationFailure::loggedOutDueToInactivity());
550
        $this->object->readCredentials();
551
    }
552
553
    public function testAuthSetUser(): void
554
    {
555
        $this->object->user = 'pmaUser2';
556
        $arr = ['host' => 'a', 'port' => 1, 'socket' => true, 'ssl' => true, 'user' => 'pmaUser2'];
557
558
        $config = Config::getInstance();
559
        $config->selectedServer = $arr;
560
        $config->selectedServer['user'] = 'pmaUser';
561
        $config->settings['Servers'][1] = $arr;
562
        $config->settings['AllowArbitraryServer'] = true;
563
        $GLOBALS['pma_auth_server'] = 'b 2';
564
        $this->object->password = 'testPW';
565
        Current::$server = 2;
566
        $config->settings['LoginCookieStore'] = 100;
567
        $GLOBALS['from_cookie'] = true;
568
        $config->set('is_https', false);
569
570
        $this->object->storeCredentials();
571
572
        $this->object->rememberCredentials();
573
574
        self::assertArrayHasKey('pmaUser-2', $_COOKIE);
575
576
        self::assertArrayHasKey('pmaAuth-2', $_COOKIE);
577
578
        $arr['password'] = 'testPW';
579
        $arr['host'] = 'b';
580
        $arr['port'] = '2';
581
        self::assertSame($arr, $config->selectedServer);
582
    }
583
584
    public function testAuthSetUserWithHeaders(): void
585
    {
586
        $this->object->user = 'pmaUser2';
587
        $arr = ['host' => 'a', 'port' => 1, 'socket' => true, 'ssl' => true, 'user' => 'pmaUser2'];
588
589
        $config = Config::getInstance();
590
        $config->selectedServer = $arr;
591
        $config->selectedServer['host'] = 'b';
592
        $config->selectedServer['user'] = 'pmaUser';
593
        $config->settings['Servers'][1] = $arr;
594
        $config->settings['AllowArbitraryServer'] = true;
595
        $config->settings['PmaAbsoluteUri'] = 'http://localhost/phpmyadmin';
596
        $GLOBALS['pma_auth_server'] = 'b 2';
597
        $this->object->password = 'testPW';
598
        $config->settings['LoginCookieStore'] = 100;
599
        $GLOBALS['from_cookie'] = false;
600
601
        $responseStub = new ResponseRendererStub();
602
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
603
604
        $this->object->storeCredentials();
605
        $response = $this->object->rememberCredentials();
606
        self::assertNotNull($response);
607
        self::assertSame(StatusCodeInterface::STATUS_FOUND, $response->getStatusCode());
608
        self::assertStringEndsWith(
609
            '/phpmyadmin/index.php?route=/&db=db&table=table&lang=en',
610
            $response->getHeaderLine('Location'),
611
        );
612
    }
613
614
    public function testAuthFailsNoPass(): void
615
    {
616
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
617
            ->disableOriginalConstructor()
618
            ->onlyMethods(['showLoginForm'])
619
            ->getMock();
620
621
        $this->object->expects(self::exactly(1))
622
            ->method('showLoginForm')
623
            ->willThrowException(new ExitException());
624
625
        $_COOKIE['pmaAuth-2'] = 'pass';
626
627
        $responseStub = new ResponseRendererStub();
628
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
629
630
        try {
631
            $this->object->showFailure(AuthenticationFailure::emptyPasswordDeniedByConfiguration());
632
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
633
        }
634
635
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
636
        $response = $responseStub->getResponse();
637
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
638
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
639
        self::assertSame(200, $response->getStatusCode());
640
641
        self::assertSame(
642
            $GLOBALS['conn_error'],
643
            'Login without a password is forbidden by configuration (see AllowNoPassword).',
644
        );
645
    }
646
647
    /** @return mixed[] */
648
    public static function dataProviderPasswordLength(): array
649
    {
650
        return [
651
            [
652
                str_repeat('a', 2001),
653
                false,
654
                'Your password is too long. To prevent denial-of-service attacks,'
655
                . ' phpMyAdmin restricts passwords to less than 2000 characters.',
656
            ],
657
            [
658
                str_repeat('a', 3000),
659
                false,
660
                'Your password is too long. To prevent denial-of-service attacks,'
661
                . ' phpMyAdmin restricts passwords to less than 2000 characters.',
662
            ],
663
            [str_repeat('a', 256), true, null],
664
            ['', true, null],
665
        ];
666
    }
667
668
    #[DataProvider('dataProviderPasswordLength')]
669
    public function testAuthFailsTooLongPass(string $password, bool $expected, string|null $connError): void
670
    {
671
        $_POST['pma_username'] = str_shuffle('123456987rootfoobar');
672
        $_POST['pma_password'] = $password;
673
674
        self::assertSame(
675
            $expected,
676
            $this->object->readCredentials(),
677
        );
678
679
        self::assertSame($GLOBALS['conn_error'], $connError);
680
    }
681
682
    public function testAuthFailsDeny(): void
683
    {
684
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
685
            ->disableOriginalConstructor()
686
            ->onlyMethods(['showLoginForm'])
687
            ->getMock();
688
689
        $this->object->expects(self::exactly(1))
690
            ->method('showLoginForm')
691
            ->willThrowException(new ExitException());
692
693
        $_COOKIE['pmaAuth-2'] = 'pass';
694
695
        $responseStub = new ResponseRendererStub();
696
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
697
698
        try {
699
            $this->object->showFailure(AuthenticationFailure::deniedByAllowDenyRules());
700
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
701
        }
702
703
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
704
        $response = $responseStub->getResponse();
705
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
706
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
707
        self::assertSame(200, $response->getStatusCode());
708
709
        self::assertSame($GLOBALS['conn_error'], 'Access denied!');
710
    }
711
712
    public function testAuthFailsActivity(): void
713
    {
714
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
715
            ->disableOriginalConstructor()
716
            ->onlyMethods(['showLoginForm'])
717
            ->getMock();
718
719
        $this->object->expects(self::exactly(1))
720
            ->method('showLoginForm')
721
            ->willThrowException(new ExitException());
722
723
        $_COOKIE['pmaAuth-2'] = 'pass';
724
725
        Config::getInstance()->settings['LoginCookieValidity'] = 10;
726
727
        $responseStub = new ResponseRendererStub();
728
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
729
730
        try {
731
            $this->object->showFailure(AuthenticationFailure::loggedOutDueToInactivity());
732
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
733
        }
734
735
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
736
        $response = $responseStub->getResponse();
737
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
738
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
739
        self::assertSame(200, $response->getStatusCode());
740
741
        self::assertSame(
742
            $GLOBALS['conn_error'],
743
            'You have been automatically logged out due to inactivity of 10 seconds.'
744
            . ' Once you log in again, you should be able to resume the work where you left off.',
745
        );
746
    }
747
748
    public function testAuthFailsDBI(): void
749
    {
750
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
751
            ->disableOriginalConstructor()
752
            ->onlyMethods(['showLoginForm'])
753
            ->getMock();
754
755
        $this->object->expects(self::exactly(1))
756
            ->method('showLoginForm')
757
            ->willThrowException(new ExitException());
758
759
        $_COOKIE['pmaAuth-2'] = 'pass';
760
761
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
762
            ->disableOriginalConstructor()
763
            ->getMock();
764
765
        $dbi->expects(self::once())
766
            ->method('getError')
767
            ->willReturn('');
768
769
        DatabaseInterface::$instance = $dbi;
770
        $GLOBALS['errno'] = 42;
771
772
        $responseStub = new ResponseRendererStub();
773
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
774
775
        try {
776
            $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
777
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
778
        }
779
780
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
781
        $response = $responseStub->getResponse();
782
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
783
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
784
        self::assertSame(200, $response->getStatusCode());
785
786
        self::assertSame($GLOBALS['conn_error'], '#42 Cannot log in to the database server.');
787
    }
788
789
    public function testAuthFailsErrno(): void
790
    {
791
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
792
            ->disableOriginalConstructor()
793
            ->onlyMethods(['showLoginForm'])
794
            ->getMock();
795
796
        $this->object->expects(self::exactly(1))
797
            ->method('showLoginForm')
798
            ->willThrowException(new ExitException());
799
800
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
801
            ->disableOriginalConstructor()
802
            ->getMock();
803
804
        $dbi->expects(self::once())
805
            ->method('getError')
806
            ->willReturn('');
807
808
        DatabaseInterface::$instance = $dbi;
809
        $_COOKIE['pmaAuth-2'] = 'pass';
810
811
        unset($GLOBALS['errno']);
812
813
        $responseStub = new ResponseRendererStub();
814
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
815
816
        try {
817
            $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
818
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
819
        }
820
821
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
822
        $response = $responseStub->getResponse();
823
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
824
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
825
        self::assertSame(200, $response->getStatusCode());
826
827
        self::assertSame($GLOBALS['conn_error'], 'Cannot log in to the database server.');
828
    }
829
830
    public function testGetEncryptionSecretEmpty(): void
831
    {
832
        $method = new ReflectionMethod(AuthenticationCookie::class, 'getEncryptionSecret');
833
834
        Config::getInstance()->settings['blowfish_secret'] = '';
835
        $_SESSION['encryption_key'] = '';
836
837
        $result = $method->invoke($this->object, null);
838
839
        self::assertSame($result, $_SESSION['encryption_key']);
840
        self::assertSame(SODIUM_CRYPTO_SECRETBOX_KEYBYTES, mb_strlen($result, '8bit'));
841
    }
842
843
    public function testGetEncryptionSecretConfigured(): void
844
    {
845
        $method = new ReflectionMethod(AuthenticationCookie::class, 'getEncryptionSecret');
846
847
        $key = str_repeat('a', SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
848
        Config::getInstance()->settings['blowfish_secret'] = $key;
849
        $_SESSION['encryption_key'] = '';
850
851
        $result = $method->invoke($this->object, null);
852
853
        self::assertSame($key, $result);
854
    }
855
856
    public function testGetSessionEncryptionSecretConfigured(): void
857
    {
858
        $method = new ReflectionMethod(AuthenticationCookie::class, 'getEncryptionSecret');
859
860
        $key = str_repeat('a', SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
861
        Config::getInstance()->settings['blowfish_secret'] = 'blowfish_secret';
862
        $_SESSION['encryption_key'] = $key;
863
864
        $result = $method->invoke($this->object, null);
865
866
        self::assertSame($key, $result);
867
    }
868
869
    public function testCookieEncryption(): void
870
    {
871
        $key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
872
        $encrypted = $this->object->cookieEncrypt('data123', $key);
873
        self::assertNotFalse(base64_decode($encrypted, true));
874
        self::assertSame('data123', $this->object->cookieDecrypt($encrypted, $key));
875
    }
876
877
    public function testCookieDecryptInvalid(): void
878
    {
879
        self::assertNull($this->object->cookieDecrypt('', ''));
880
881
        $key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
882
        $encrypted = $this->object->cookieEncrypt('data123', $key);
883
        self::assertSame('data123', $this->object->cookieDecrypt($encrypted, $key));
884
885
        self::assertNull($this->object->cookieDecrypt('', $key));
886
        self::assertNull($this->object->cookieDecrypt($encrypted, ''));
887
        self::assertNull($this->object->cookieDecrypt($encrypted, random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES)));
888
    }
889
890
    /** @throws ReflectionException */
891
    public function testPasswordChange(): void
892
    {
893
        $newPassword = 'PMAPASSWD2';
894
        $config = Config::getInstance();
895
        $config->set('is_https', false);
896
        $config->settings['AllowArbitraryServer'] = true;
897
        $GLOBALS['pma_auth_server'] = 'b 2';
898
        $_SESSION['encryption_key'] = '';
899
        $_COOKIE = [];
900
901
        $this->object->handlePasswordChange($newPassword);
902
903
        $payload = ['password' => $newPassword, 'server' => 'b 2'];
904
905
        /** @psalm-suppress EmptyArrayAccess */
906
        self::assertIsString($_COOKIE['pmaAuth-' . Current::$server]);
907
        $decryptedCookie = $this->object->cookieDecrypt(
908
            $_COOKIE['pmaAuth-' . Current::$server],
909
            $_SESSION['encryption_key'],
910
        );
911
        self::assertSame(json_encode($payload), $decryptedCookie);
912
    }
913
914
    public function testAuthenticate(): void
915
    {
916
        $config = Config::getInstance();
917
        $config->settings['CaptchaApi'] = '';
918
        $config->settings['CaptchaRequestParam'] = '';
919
        $config->settings['CaptchaResponseParam'] = '';
920
        $config->settings['CaptchaLoginPrivateKey'] = '';
921
        $config->settings['CaptchaLoginPublicKey'] = '';
922
        $config->selectedServer['AllowRoot'] = false;
923
        $config->selectedServer['AllowNoPassword'] = false;
924
        $_REQUEST['old_usr'] = '';
925
        $_POST['pma_username'] = 'testUser';
926
        $_POST['pma_password'] = 'testPassword';
927
928
        ob_start();
929
        $response = $this->object->authenticate();
930
        $result = ob_get_clean();
931
932
        self::assertNull($response);
933
        /* Nothing should be printed */
934
        self::assertSame('', $result);
935
936
        /* Verify readCredentials worked */
937
        self::assertSame('testUser', $this->object->user);
938
        self::assertSame('testPassword', $this->object->password);
939
940
        /* Verify storeCredentials worked */
941
        self::assertSame('testUser', $config->selectedServer['user']);
942
        self::assertSame('testPassword', $config->selectedServer['password']);
943
    }
944
945
    /**
946
     * @param string  $user     user
947
     * @param string  $pass     pass
948
     * @param string  $ip       ip
949
     * @param bool    $root     root
950
     * @param bool    $nopass   nopass
951
     * @param mixed[] $rules    rules
952
     * @param string  $expected expected result
953
     */
954
    #[DataProvider('checkRulesProvider')]
955
    public function testCheckRules(
956
        string $user,
957
        string $pass,
958
        string $ip,
959
        bool $root,
960
        bool $nopass,
961
        array $rules,
962
        string $expected,
963
    ): void {
964
        $this->object->user = $user;
965
        $this->object->password = $pass;
966
        $this->object->storeCredentials();
967
968
        $_SERVER['REMOTE_ADDR'] = $ip;
969
970
        $config = Config::getInstance();
971
        $config->selectedServer['AllowRoot'] = $root;
972
        $config->selectedServer['AllowNoPassword'] = $nopass;
973
        $config->selectedServer['AllowDeny'] = $rules;
974
975
        $exception = null;
976
        try {
977
            $this->object->checkRules();
978
        } catch (AuthenticationFailure $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
979
        }
980
981
        if ($expected === '') {
982
            self::assertNull($exception, 'checkRules() should not throw an exception.');
983
984
            return;
985
        }
986
987
        self::assertInstanceOf(AuthenticationFailure::class, $exception);
988
        self::assertSame($expected, $exception->failureType);
989
    }
990
991
    /** @return mixed[] */
992
    public static function checkRulesProvider(): array
993
    {
994
        return [
995
            'nopass-ok' => ['testUser', '', '1.2.3.4', true, true, [], ''],
996
            'nopass' => ['testUser', '', '1.2.3.4', true, false, [], AuthenticationFailure::EMPTY_DENIED],
997
            'root-ok' => ['root', 'root', '1.2.3.4', true, true, [], ''],
998
            'root' => ['root', 'root', '1.2.3.4', false, true, [], AuthenticationFailure::ROOT_DENIED],
999
            'rules-deny-allow-ok' => [
1000
                'root',
1001
                'root',
1002
                '1.2.3.4',
1003
                true,
1004
                true,
1005
                ['order' => 'deny,allow', 'rules' => ['allow root 1.2.3.4', 'deny % from all']],
1006
                '',
1007
            ],
1008
            'rules-deny-allow-reject' => [
1009
                'user',
1010
                'root',
1011
                '1.2.3.4',
1012
                true,
1013
                true,
1014
                ['order' => 'deny,allow', 'rules' => ['allow root 1.2.3.4', 'deny % from all']],
1015
                AuthenticationFailure::ALLOW_DENIED,
1016
            ],
1017
            'rules-allow-deny-ok' => [
1018
                'root',
1019
                'root',
1020
                '1.2.3.4',
1021
                true,
1022
                true,
1023
                ['order' => 'allow,deny', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
1024
                '',
1025
            ],
1026
            'rules-allow-deny-reject' => [
1027
                'user',
1028
                'root',
1029
                '1.2.3.4',
1030
                true,
1031
                true,
1032
                ['order' => 'allow,deny', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
1033
                AuthenticationFailure::ALLOW_DENIED,
1034
            ],
1035
            'rules-explicit-ok' => [
1036
                'root',
1037
                'root',
1038
                '1.2.3.4',
1039
                true,
1040
                true,
1041
                ['order' => 'explicit', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
1042
                '',
1043
            ],
1044
            'rules-explicit-reject' => [
1045
                'user',
1046
                'root',
1047
                '1.2.3.4',
1048
                true,
1049
                true,
1050
                ['order' => 'explicit', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
1051
                AuthenticationFailure::ALLOW_DENIED,
1052
            ],
1053
        ];
1054
    }
1055
}
1056