Issues (386)

Security Analysis    21 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  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.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  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.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation (2)
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection (1)
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.
  File Exposure (4)
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.
  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.
  Code Injection (13)
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  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.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  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.
  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.
  Cross-Site Scripting (1)
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.
  Header Injection
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.

modules/core/src/Controller/Login.php (6 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\core\Controller;
6
7
use SimpleSAML\{Auth, Configuration, Error, Module, Utils};
8
use SimpleSAML\Module\core\Auth\{UserPassBase, UserPassOrgBase};
9
use SimpleSAML\XHTML\Template;
10
use Symfony\Component\HttpFoundation\{Cookie, RedirectResponse, Request, Response};
11
use SimpleSAML\Error\ErrorCodes;
12
13
use function array_key_exists;
14
use function substr;
15
use function strval;
16
use function time;
17
use function trim;
18
19
/**
20
 * Controller class for the core module.
21
 *
22
 * This class serves the different views available in the module.
23
 *
24
 * @package SimpleSAML\Module\core
25
 */
26
class Login
27
{
28
    /**
29
     * @var \SimpleSAML\Auth\Source|string
30
     * @psalm-var \SimpleSAML\Auth\Source|class-string
31
     */
32
    protected $authSource = Auth\Source::class;
33
34
    /**
35
     * @var \SimpleSAML\Auth\State|string
36
     * @psalm-var \SimpleSAML\Auth\State|class-string
37
     */
38
    protected $authState = Auth\State::class;
39
40
    /**
41
     * These are all the subclass instances of ErrorCodes which have been created
42
     */
43
    protected static array $registeredErrorCodeClasses = [];
44
45
46
47
    /**
48
     * Controller constructor.
49
     *
50
     * It initializes the global configuration for the controllers implemented here.
51
     *
52
     * @param \SimpleSAML\Configuration              $config The configuration to use by the controllers.
53
     *
54
     * @throws \Exception
55
     */
56
    public function __construct(
57
        protected Configuration $config,
58
    ) {
59
    }
60
61
62
    /**
63
     * Inject the \SimpleSAML\Auth\Source dependency.
64
     *
65
     * @param \SimpleSAML\Auth\Source $authSource
66
     */
67
    public function setAuthSource(Auth\Source $authSource): void
68
    {
69
        $this->authSource = $authSource;
70
    }
71
72
73
    /**
74
     * Inject the \SimpleSAML\Auth\State dependency.
75
     *
76
     * @param \SimpleSAML\Auth\State $authState
77
     */
78
    public function setAuthState(Auth\State $authState): void
79
    {
80
        $this->authState = $authState;
81
    }
82
83
84
    /**
85
     * @return \SimpleSAML\XHTML\Template
86
     */
87
    public function welcome(): Template
88
    {
89
        return new Template($this->config, 'core:welcome.twig');
90
    }
91
92
93
    /**
94
     * This page shows a username/password login form, and passes information from it
95
     * to the \SimpleSAML\Module\core\Auth\UserPassBase class, which is a generic class for
96
     * username/password authentication.
97
     *
98
     * @param \Symfony\Component\HttpFoundation\Request $request
99
     * @return \Symfony\Component\HttpFoundation\Response
100
     */
101
    public function loginuserpass(Request $request): Response
102
    {
103
        // Retrieve the authentication state
104
        if (!$request->query->has('AuthState')) {
105
            throw new Error\BadRequest('Missing AuthState parameter.');
106
        }
107
        $authStateId = $request->query->get('AuthState');
108
        $this->authState::validateStateId($authStateId);
109
110
        $state = $this->authState::loadState($authStateId, UserPassBase::STAGEID);
111
112
        /** @var \SimpleSAML\Module\core\Auth\UserPassBase|null $source */
113
        $source = $this->authSource::getById($state[UserPassBase::AUTHID]);
114
        if ($source === null) {
115
            throw new Error\Exception(
116
                'Could not find authentication source with id ' . $state[UserPassBase::AUTHID],
117
            );
118
        }
119
120
        return $this->handleLogin($request, $source, $state);
0 ignored issues
show
It seems like $state can also be of type null; however, parameter $state of SimpleSAML\Module\core\C...er\Login::handleLogin() does only seem to accept array, 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

120
        return $this->handleLogin($request, $source, /** @scrutinizer ignore-type */ $state);
Loading history...
121
    }
122
123
124
    /**
125
     * Called by the constructor in ErrorCode to register subclasses with us
126
     * so we can track which subclasses are valid names in order to limit
127
     * which classes we might recreate
128
     *
129
     * @para object ecc an instance of an ErrorCode or subclass
130
     */
131
    public static function registerErrorCodeClass(ErrorCodes $ecc): void
132
    {
133
        if (is_subclass_of($ecc, ErrorCodes::class, false)) {
134
            $className = get_class($ecc);
135
            self::$registeredErrorCodeClasses[] = $className;
136
        }
137
    }
138
139
    /**
140
     * This method handles the generic part for both loginuserpass and loginuserpassorg
141
     *
142
     * @param \Symfony\Component\HttpFoundation\Request $request
143
     * @param \SimpleSAML\Module\core\Auth\UserPassBase|\SimpleSAML\Module\core\Auth\UserPassOrgBase $source
144
     * @param array $state
145
     * @return \Symfony\Component\HttpFoundation\Response
146
     */
147
    private function handleLogin(Request $request, UserPassBase|UserPassOrgBase $source, array $state): Response
148
    {
149
        $authStateId = $request->query->get('AuthState');
150
        $this->authState::validateStateId($authStateId);
151
152
        $organizations = $organization = null;
153
        if ($source instanceof UserPassOrgBase) {
154
            $organizations = UserPassOrgBase::listOrganizations($authStateId);
155
            $organization = $this->getOrganizationFromRequest($request, $source, $state);
156
        }
157
158
        $username = $this->getUsernameFromRequest($request, $source, $state);
159
        $password = $this->getPasswordFromRequest($request);
160
161
        $errorCode = null;
162
        $errorParams = null;
163
        $codeClass = '';
164
165
        if (isset($state['error'])) {
166
            $errorCode = $state['error']['code'];
167
            $errorParams = $state['error']['params'];
168
            $codeClass = $state['error']['codeclass'];
169
        }
170
171
        if ($organizations === null || $organization !== '') {
172
            if (!empty($username) || !empty($password)) {
173
                $cookies = [];
174
                $httpUtils = new Utils\HTTP();
175
                $sameSiteNone = $httpUtils->canSetSamesiteNone() ? Cookie::SAMESITE_NONE : null;
176
177
                // Either username or password set - attempt to log in
178
                if (array_key_exists('forcedUsername', $state) && ($state['forcedUsername'] !== false)) {
179
                    $username = $state['forcedUsername'];
180
                }
181
182
                if ($source->getRememberUsernameEnabled()) {
183
                    if (
184
                        $request->request->has('remember_username')
185
                        && ($request->request->get('remember_username') === 'Yes')
186
                    ) {
187
                        $expire = time() + 3153600;
188
                    } else {
189
                        $expire = time() - 300;
190
                    }
191
192
                    $cookies[] = $this->renderCookie(
193
                        $source->getAuthId() . '-username',
194
                        $username,
195
                        $expire,
196
                        '/',   // path
197
                        null,  // domain
198
                        null,  // secure
199
                        true,  // httponly
200
                        false, // raw
201
                        $sameSiteNone,
202
                    );
203
                }
204
205
                if (($source instanceof UserPassBase) && $source->isRememberMeEnabled()) {
206
                    if ($request->request->has('remember_me') && ($request->request->get('remember_me') === 'Yes')) {
207
                        $state['RememberMe'] = true;
208
                        $authStateId = Auth\State::saveState($state, UserPassBase::STAGEID);
209
                    }
210
                }
211
212
                if (($source instanceof UserPassOrgBase) && $source->getRememberOrganizationEnabled()) {
213
                    if (
214
                        $request->request->has('remember_organization')
215
                        && ($request->request->get('remember_organization') === 'Yes')
216
                    ) {
217
                        $expire = time() + 3153600;
218
                    } else {
219
                        $expire = time() - 300;
220
                    }
221
222
                    $cookies[] = $this->renderCookie(
223
                        $source->getAuthId() . '-organization',
224
                        $organization,
225
                        $expire,
226
                        '/',   // path
227
                        null,  // domain
228
                        null,  // secure
229
                        true,  // httponly
230
                        false, // raw
231
                        $sameSiteNone,
232
                    );
233
                }
234
235
                try {
236
                    if ($source instanceof UserPassOrgBase) {
237
                        $response = UserPassOrgBase::handleLogin($authStateId, $username, $password, $organization);
0 ignored issues
show
It seems like $organization can also be of type null; however, parameter $organization of SimpleSAML\Module\core\A...sOrgBase::handleLogin() 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

237
                        $response = UserPassOrgBase::handleLogin($authStateId, $username, $password, /** @scrutinizer ignore-type */ $organization);
Loading history...
238
                    } else {
239
                        $response = UserPassBase::handleLogin($authStateId, $username, $password);
240
                    }
241
242
                    foreach ($cookies as $cookie) {
243
                        $response->headers->setCookie($cookie);
244
                    }
245
246
                    return $response;
247
                } catch (Error\Error $e) {
248
                    // Login failed. Extract error code and parameters, to display the error
249
                    $errorCode = $e->getErrorCode();
250
                    $errorParams = $e->getParameters();
251
                    $codeClass = get_class($e->getErrorCodes());
252
253
                    $state['error'] = [
254
                        'code' => $errorCode,
255
                        'params' => $errorParams,
256
                        'codeclass' => $codeClass,
257
                    ];
258
                    $authStateId = Auth\State::saveState($state, $source::STAGEID);
259
                }
260
261
                if (isset($state['error'])) {
262
                    unset($state['error']);
263
                }
264
            }
265
        }
266
267
        $t = new Template($this->config, 'core:loginuserpass.twig');
268
269
        if ($source instanceof UserPassOrgBase) {
270
            $t->data['username'] = $state['core:username'] ?? '';
271
            $t->data['forceUsername'] = false;
272
            $t->data['rememberUsernameEnabled'] = $source->getRememberUsernameEnabled();
273
            $t->data['rememberUsernameChecked'] = $source->getRememberUsernameChecked();
274
            $t->data['rememberMeEnabled'] = false;
275
            $t->data['rememberMeChecked'] = false;
276
        } elseif (array_key_exists('forcedUsername', $state)) {
277
            $t->data['username'] = $state['forcedUsername'];
278
            $t->data['forceUsername'] = true;
279
            $t->data['rememberUsernameEnabled'] = false;
280
            $t->data['rememberUsernameChecked'] = false;
281
            $t->data['rememberMeEnabled'] = $source->isRememberMeEnabled();
282
            $t->data['rememberMeChecked'] = $source->isRememberMeChecked();
283
        } else {
284
            $t->data['username'] = $state['core:username'] ?? '';
285
            $t->data['forceUsername'] = false;
286
            $t->data['rememberUsernameEnabled'] = $source->getRememberUsernameEnabled();
287
            $t->data['rememberUsernameChecked'] = $source->getRememberUsernameChecked();
288
            $t->data['rememberMeEnabled'] = $source->isRememberMeEnabled();
289
            $t->data['rememberMeChecked'] = $source->isRememberMeChecked();
290
291
            if ($request->cookies->has($source->getAuthId() . '-username')) {
292
                $t->data['rememberUsernameChecked'] = true;
293
            }
294
        }
295
296
        if ($source instanceof UserPassOrgBase) {
297
            $t->data['formURL'] = Module::getModuleURL('core/loginuserpassorg', ['AuthState' => $authStateId]);
298
            if ($request->request->has($source->getAuthId() . '-username')) {
299
                $t->data['rememberUsernameChecked'] = true;
300
            }
301
302
            $t->data['rememberOrganizationEnabled'] = $source->getRememberOrganizationEnabled();
303
            $t->data['rememberOrganizationChecked'] = $source->getRememberOrganizationChecked();
304
305
            if ($request->request->has($source->getAuthId() . '-organization')) {
306
                $t->data['rememberOrganizationChecked'] = true;
307
            }
308
309
            if ($organizations !== null) {
310
                $t->data['selectedOrg'] = $organization;
311
                $t->data['organizations'] = $organizations;
312
            }
313
        } else {
314
            $t->data['formURL'] = Module::getModuleURL('core/loginuserpass', ['AuthState' => $authStateId]);
315
            $t->data['loginpage_links'] = $source->getLoginLinks();
316
        }
317
318
        $t->data['errorcode'] = $errorCode;
319
        $t->data['errorcodes'] = (new Error\ErrorCodes())->getAllMessages();
320
        $t->data['errorparams'] = $errorParams;
321
322
        $className = $codeClass;
323
        if ($className) {
324
            if (in_array($className, self::$registeredErrorCodeClasses)) {
325
                if (!class_exists($className)) {
326
                    throw new Error\Exception("Could not resolve error class. no class named '$className'.");
327
                }
328
329
                if (!is_subclass_of($className, ErrorCodes::class)) {
330
                    throw new Error\Exception(
331
                        'Could not resolve error class: The class \'' . $className
332
                        . '\' isn\'t a subclass of \'' . ErrorCodes::class . '\'.',
333
                    );
334
                }
335
336
                $obj = Module::createObject($className, ErrorCodes::class);
337
                $t->data['errorcodes'] = $obj->getAllMessages();
338
            } else {
339
                if ($className != ErrorCodes::class) {
340
                    throw new Error\Exception(
341
                        'The desired error code class is not found or of the wrong type ' . $className,
342
                    );
343
                }
344
            }
345
        }
346
347
        if (isset($state['SPMetadata'])) {
348
            $t->data['SPMetadata'] = $state['SPMetadata'];
349
        } else {
350
            $t->data['SPMetadata'] = null;
351
        }
352
353
        return $t;
354
    }
355
356
357
    /**
358
     * This page shows a username/password/organization login form, and passes information from
359
     * into the \SimpleSAML\Module\core\Auth\UserPassBase class, which is a generic class for
360
     * username/password/organization authentication.
361
     *
362
     * @param \Symfony\Component\HttpFoundation\Request $request
363
     * @return \Symfony\Component\HttpFoundation\Response
364
     */
365
    public function loginuserpassorg(Request $request): Response
366
    {
367
        // Retrieve the authentication state
368
        if (!$request->query->has('AuthState')) {
369
            throw new Error\BadRequest('Missing AuthState parameter.');
370
        }
371
        $authStateId = $request->query->get('AuthState');
372
        $this->authState::validateStateId($authStateId);
373
374
        $state = $this->authState::loadState($authStateId, UserPassOrgBase::STAGEID);
375
376
        /** @var \SimpleSAML\Module\core\Auth\UserPassOrgBase $source */
377
        $source = $this->authSource::getById($state[UserPassOrgBase::AUTHID]);
378
        if ($source === null) {
379
            throw new Error\Exception(
380
                'Could not find authentication source with id ' . $state[UserPassOrgBase::AUTHID],
381
            );
382
        }
383
384
        return $this->handleLogin($request, $source, $state);
0 ignored issues
show
It seems like $state can also be of type null; however, parameter $state of SimpleSAML\Module\core\C...er\Login::handleLogin() does only seem to accept array, 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

384
        return $this->handleLogin($request, $source, /** @scrutinizer ignore-type */ $state);
Loading history...
385
    }
386
387
388
    /**
389
     * @param string $name     The name for the cookie
390
     * @param string $value    The value for the cookie
391
     * @param int $expire      The expiration in seconds
392
     * @param string $path     The path for the cookie
393
     * @param string $domain   The domain for the cookie
394
     * @param bool $secure     Whether this cookie must have the secure-flag
395
     * @param bool $httponly   Whether this cookie must have the httponly-flag
396
     * @param bool $raw        Whether this cookie must be sent without urlencoding
397
     * @param string $sameSite The value for the sameSite-flag
398
     * @return \Symfony\Component\HttpFoundation\Cookie
399
     */
400
    private function renderCookie(
401
        string $name,
402
        ?string $value,
403
        int $expire = 0,
404
        string $path = '/',
405
        ?string $domain = null,
406
        ?bool $secure = null,
407
        bool $httponly = true,
408
        bool $raw = false,
409
        ?string $sameSite = 'none',
410
    ): Cookie {
411
        return new Cookie($name, $value, $expire, $path, $domain, $secure, $httponly, $raw, $sameSite);
412
    }
413
414
415
    /**
416
     * Retrieve the username from the request, a cookie or the state
417
     *
418
     * @param \Symfony\Component\HttpFoundation\Request $request
419
     * @param \SimpleSAML\Auth\Source $source
420
     * @param array $state
421
     * @return string
422
     */
423
    private function getUsernameFromRequest(Request $request, Auth\Source $source, array $state): string
0 ignored issues
show
The parameter $state is not used and could be removed. ( Ignorable by Annotation )

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

423
    private function getUsernameFromRequest(Request $request, Auth\Source $source, /** @scrutinizer ignore-unused */ array $state): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
424
    {
425
        $username = '';
426
427
        if ($request->request->has('username')) {
428
            $username = trim($request->request->get('username'));
429
        } elseif (
430
            $source->getRememberUsernameEnabled()
0 ignored issues
show
The method getRememberUsernameEnabled() does not exist on SimpleSAML\Auth\Source. It seems like you code against a sub-type of SimpleSAML\Auth\Source such as SimpleSAML\Module\core\Auth\UserPassOrgBase or SimpleSAML\Module\core\Auth\UserPassBase. ( Ignorable by Annotation )

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

430
            $source->/** @scrutinizer ignore-call */ 
431
                     getRememberUsernameEnabled()
Loading history...
431
            && $request->cookies->has($source->getAuthId() . '-username')
432
        ) {
433
            $username = $request->cookies->get($source->getAuthId() . '-username');
434
        }
435
436
        return $username;
437
    }
438
439
440
    /**
441
     * Retrieve the password from the request
442
     *
443
     * @param \Symfony\Component\HttpFoundation\Request $request
444
     * @return string
445
     */
446
    private function getPasswordFromRequest(Request $request): string
447
    {
448
        $password = '';
449
450
        if ($request->request->has('password')) {
451
            $password = $request->request->get('password');
452
        }
453
454
        return $password;
455
    }
456
457
458
    /**
459
     * Retrieve the organization from the request, a cookie or the state
460
     *
461
     * @param \Symfony\Component\HttpFoundation\Request $request
462
     * @param \SimpleSAML\Auth\Source $source
463
     * @param array $state
464
     * @return string
465
     */
466
    private function getOrganizationFromRequest(Request $request, Auth\Source $source, array $state): string
467
    {
468
        $organization = '';
469
470
        if ($request->request->has('organization')) {
471
            $organization = $request->request->get('organization');
472
        } elseif (
473
            $source->getRememberOrganizationEnabled()
0 ignored issues
show
The method getRememberOrganizationEnabled() does not exist on SimpleSAML\Auth\Source. It seems like you code against a sub-type of SimpleSAML\Auth\Source such as SimpleSAML\Module\core\Auth\UserPassOrgBase. ( Ignorable by Annotation )

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

473
            $source->/** @scrutinizer ignore-call */ 
474
                     getRememberOrganizationEnabled()
Loading history...
474
            && $request->cookies->has($source->getAuthId() . '-organization')
475
        ) {
476
            $organization = $request->cookies->get($source->getAuthId() . '-organization');
477
        } elseif (isset($state['core:organization'])) {
478
            $organization = strval($state['core:organization']);
479
        }
480
481
        return $organization;
482
    }
483
484
485
    /**
486
     * Searches for a valid and allowed ReturnTo URL parameter,
487
     * otherwise give the base installation page as a return point.
488
     */
489
    private function getReturnPath(Request $request): string
490
    {
491
        $httpUtils = new Utils\HTTP();
492
493
        $returnTo = $request->query->get('ReturnTo', false);
494
        if ($returnTo !== false) {
495
            $returnTo = $httpUtils->checkURLAllowed($returnTo);
496
        }
497
        if (empty($returnTo)) {
498
            return $this->config->getBasePath();
499
        }
500
        return $returnTo;
501
    }
502
503
504
    /**
505
     * This clears the user's IdP discovery choices.
506
     *
507
     * @param Request $request The request that lead to this login operation.
508
     */
509
    public function cleardiscochoices(Request $request): RedirectResponse
510
    {
511
        $httpUtils = new Utils\HTTP();
512
513
        // The base path for cookies. This should be the installation directory for SimpleSAMLphp.
514
        $cookiePath = $this->config->getBasePath();
515
516
        // We delete all cookies which starts with 'idpdisco_'
517
        foreach ($request->cookies->all() as $cookieName => $value) {
518
            if (substr($cookieName, 0, 9) !== 'idpdisco_') {
519
                // Not a idpdisco cookie.
520
                continue;
521
            }
522
523
            $httpUtils->setCookie($cookieName, null, ['path' => $cookiePath, 'httponly' => false], false);
524
        }
525
526
        $returnTo = $this->getReturnPath($request);
527
528
        // Redirect to destination.
529
        return $httpUtils->redirectTrustedURL($returnTo);
530
    }
531
}
532