Issues (378)

Security Analysis    23 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 (5)
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 (14)
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 Exception as BuiltinException;
8
use SimpleSAML\{Auth, Configuration, Error, Module, Utils};
9
use SimpleSAML\Module\core\Auth\{UserPassBase, UserPassOrgBase};
10
use SimpleSAML\XHTML\Template;
11
use Symfony\Component\HttpFoundation\{Cookie, RedirectResponse, Request, Response};
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
    /**
42
     * Controller constructor.
43
     *
44
     * It initializes the global configuration for the controllers implemented here.
45
     *
46
     * @param \SimpleSAML\Configuration              $config The configuration to use by the controllers.
47
     *
48
     * @throws \Exception
49
     */
50
    public function __construct(
51
        protected Configuration $config
52
    ) {
53
    }
54
55
56
    /**
57
     * Inject the \SimpleSAML\Auth\Source dependency.
58
     *
59
     * @param \SimpleSAML\Auth\Source $authSource
60
     */
61
    public function setAuthSource(Auth\Source $authSource): void
62
    {
63
        $this->authSource = $authSource;
64
    }
65
66
67
    /**
68
     * Inject the \SimpleSAML\Auth\State dependency.
69
     *
70
     * @param \SimpleSAML\Auth\State $authState
71
     */
72
    public function setAuthState(Auth\State $authState): void
73
    {
74
        $this->authState = $authState;
75
    }
76
77
78
    /**
79
     * @return \SimpleSAML\XHTML\Template
80
     */
81
    public function welcome(): Template
82
    {
83
        return new Template($this->config, 'core:welcome.twig');
84
    }
85
86
87
    /**
88
     * This page shows a username/password login form, and passes information from it
89
     * to the \SimpleSAML\Module\core\Auth\UserPassBase class, which is a generic class for
90
     * username/password authentication.
91
     *
92
     * @param \Symfony\Component\HttpFoundation\Request $request
93
     * @return \Symfony\Component\HttpFoundation\Response
94
     */
95
    public function loginuserpass(Request $request): Response
96
    {
97
        // Retrieve the authentication state
98
        if (!$request->query->has('AuthState')) {
99
            throw new Error\BadRequest('Missing AuthState parameter.');
100
        }
101
        $authStateId = $request->query->get('AuthState');
102
        $this->authState::validateStateId($authStateId);
103
104
        $state = $this->authState::loadState($authStateId, UserPassBase::STAGEID);
105
106
        /** @var \SimpleSAML\Module\core\Auth\UserPassBase|null $source */
107
        $source = $this->authSource::getById($state[UserPassBase::AUTHID]);
108
        if ($source === null) {
109
            throw new BuiltinException(
110
                'Could not find authentication source with id ' . $state[UserPassBase::AUTHID]
111
            );
112
        }
113
114
        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

114
        return $this->handleLogin($request, $source, /** @scrutinizer ignore-type */ $state);
Loading history...
115
    }
116
117
118
    /**
119
     * This method handles the generic part for both loginuserpass and loginuserpassorg
120
     *
121
     * @param \Symfony\Component\HttpFoundation\Request $request
122
     * @param \SimpleSAML\Module\core\Auth\UserPassBase|\SimpleSAML\Module\core\Auth\UserPassOrgBase $source
123
     * @param array $state
124
     * @return \Symfony\Component\HttpFoundation\Response
125
     */
126
    private function handleLogin(Request $request, UserPassBase|UserPassOrgBase $source, array $state): Response
127
    {
128
        $authStateId = $request->query->get('AuthState');
129
        $this->authState::validateStateId($authStateId);
130
131
        $organizations = $organization = null;
132
        if ($source instanceof UserPassOrgBase) {
133
            $organizations = UserPassOrgBase::listOrganizations($authStateId);
134
            $organization = $this->getOrganizationFromRequest($request, $source, $state);
135
        }
136
137
        $username = $this->getUsernameFromRequest($request, $source, $state);
138
        $password = $this->getPasswordFromRequest($request);
139
140
        $errorCode = null;
141
        $errorParams = null;
142
143
        if (isset($state['error'])) {
144
            $errorCode = $state['error']['code'];
145
            $errorParams = $state['error']['params'];
146
        }
147
148
        if ($organizations === null || $organization !== '') {
149
            if (!empty($username) || !empty($password)) {
150
                $cookies = [];
151
                $httpUtils = new Utils\HTTP();
152
                $sameSiteNone = $httpUtils->canSetSamesiteNone() ? Cookie::SAMESITE_NONE : null;
153
154
                // Either username or password set - attempt to log in
155
                if (array_key_exists('forcedUsername', $state) && ($state['forcedUsername'] !== false)) {
156
                    $username = $state['forcedUsername'];
157
                }
158
159
                if ($source->getRememberUsernameEnabled()) {
160
                    if (
161
                        $request->request->has('remember_username')
162
                        && ($request->request->get('remember_username') === 'Yes')
163
                    ) {
164
                        $expire = time() + 3153600;
165
                    } else {
166
                        $expire = time() - 300;
167
                    }
168
169
                    $cookies[] = $this->renderCookie(
170
                        $source->getAuthId() . '-username',
171
                        $username,
172
                        $expire,
173
                        '/',   // path
174
                        null,  // domain
175
                        null,  // secure
176
                        true,  // httponly
177
                        false, // raw
178
                        $sameSiteNone,
179
                    );
180
                }
181
182
                if (($source instanceof UserPassBase) && $source->isRememberMeEnabled()) {
183
                    if ($request->request->has('remember_me') && ($request->request->get('remember_me') === 'Yes')) {
184
                        $state['RememberMe'] = true;
185
                        $authStateId = Auth\State::saveState($state, UserPassBase::STAGEID);
186
                    }
187
                }
188
189
                if (($source instanceof UserPassOrgBase) && $source->getRememberOrganizationEnabled()) {
190
                    if (
191
                        $request->request->has('remember_organization')
192
                        && ($request->request->get('remember_organization') === 'Yes')
193
                    ) {
194
                        $expire = time() + 3153600;
195
                    } else {
196
                        $expire = time() - 300;
197
                    }
198
199
                    $cookies[] = $this->renderCookie(
200
                        $source->getAuthId() . '-organization',
201
                        $organization,
202
                        $expire,
203
                        '/',   // path
204
                        null,  // domain
205
                        null,  // secure
206
                        true,  // httponly
207
                        false, // raw
208
                        $sameSiteNone,
209
                    );
210
                }
211
212
                try {
213
                    if ($source instanceof UserPassOrgBase) {
214
                        $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

214
                        $response = UserPassOrgBase::handleLogin($authStateId, $username, $password, /** @scrutinizer ignore-type */ $organization);
Loading history...
215
                    } else {
216
                        $response = UserPassBase::handleLogin($authStateId, $username, $password);
217
                    }
218
219
                    foreach ($cookies as $cookie) {
220
                        $response->headers->setCookie($cookie);
221
                    }
222
223
                    return $response;
224
                } catch (Error\Error $e) {
225
                    // Login failed. Extract error code and parameters, to display the error
226
                    $errorCode = $e->getErrorCode();
227
                    $errorParams = $e->getParameters();
228
                    $state['error'] = [
229
                        'code' => $errorCode,
230
                        'params' => $errorParams
231
                    ];
232
                    $authStateId = Auth\State::saveState($state, $source::STAGEID);
233
                }
234
235
                if (isset($state['error'])) {
236
                    unset($state['error']);
237
                }
238
            }
239
        }
240
241
        $t = new Template($this->config, 'core:loginuserpass.twig');
242
243
        if ($source instanceof UserPassOrgBase) {
244
            $t->data['username'] = $username;
245
            $t->data['forceUsername'] = false;
246
            $t->data['rememberUsernameEnabled'] = $source->getRememberUsernameEnabled();
247
            $t->data['rememberUsernameChecked'] = $source->getRememberUsernameChecked();
248
            $t->data['rememberMeEnabled'] = false;
249
            $t->data['rememberMeChecked'] = false;
250
        } elseif (array_key_exists('forcedUsername', $state)) {
251
            $t->data['username'] = $state['forcedUsername'];
252
            $t->data['forceUsername'] = true;
253
            $t->data['rememberUsernameEnabled'] = false;
254
            $t->data['rememberUsernameChecked'] = false;
255
            $t->data['rememberMeEnabled'] = $source->isRememberMeEnabled();
256
            $t->data['rememberMeChecked'] = $source->isRememberMeChecked();
257
        } else {
258
            $t->data['username'] = $username;
259
            $t->data['forceUsername'] = false;
260
            $t->data['rememberUsernameEnabled'] = $source->getRememberUsernameEnabled();
261
            $t->data['rememberUsernameChecked'] = $source->getRememberUsernameChecked();
262
            $t->data['rememberMeEnabled'] = $source->isRememberMeEnabled();
263
            $t->data['rememberMeChecked'] = $source->isRememberMeChecked();
264
265
            if ($request->cookies->has($source->getAuthId() . '-username')) {
266
                $t->data['rememberUsernameChecked'] = true;
267
            }
268
        }
269
270
        if ($source instanceof UserPassOrgBase) {
271
            $t->data['formURL'] = Module::getModuleURL('core/loginuserpassorg', ['AuthState' => $authStateId]);
272
            if ($request->request->has($source->getAuthId() . '-username')) {
273
                $t->data['rememberUsernameChecked'] = true;
274
            }
275
276
            $t->data['rememberOrganizationEnabled'] = $source->getRememberOrganizationEnabled();
277
            $t->data['rememberOrganizationChecked'] = $source->getRememberOrganizationChecked();
278
279
            if ($request->request->has($source->getAuthId() . '-organization')) {
280
                $t->data['rememberOrganizationChecked'] = true;
281
            }
282
283
            if ($organizations !== null) {
284
                $t->data['selectedOrg'] = $organization;
285
                $t->data['organizations'] = $organizations;
286
            }
287
        } else {
288
            $t->data['formURL'] = Module::getModuleURL('core/loginuserpass', ['AuthState' => $authStateId]);
289
            $t->data['loginpage_links'] = $source->getLoginLinks();
290
        }
291
292
        $t->data['errorcode'] = $errorCode;
293
        $t->data['errorcodes'] = Error\ErrorCodes::getAllErrorCodeMessages();
0 ignored issues
show
The method getAllErrorCodeMessages() does not exist on SimpleSAML\Error\ErrorCodes. ( Ignorable by Annotation )

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

293
        /** @scrutinizer ignore-call */ 
294
        $t->data['errorcodes'] = Error\ErrorCodes::getAllErrorCodeMessages();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
294
        $t->data['errorparams'] = $errorParams;
295
296
        if (isset($state['SPMetadata'])) {
297
            $t->data['SPMetadata'] = $state['SPMetadata'];
298
        } else {
299
            $t->data['SPMetadata'] = null;
300
        }
301
302
        return $t;
303
    }
304
305
306
    /**
307
     * This page shows a username/password/organization login form, and passes information from
308
     * into the \SimpleSAML\Module\core\Auth\UserPassBase class, which is a generic class for
309
     * username/password/organization authentication.
310
     *
311
     * @param \Symfony\Component\HttpFoundation\Request $request
312
     * @return \Symfony\Component\HttpFoundation\Response
313
     */
314
    public function loginuserpassorg(Request $request): Response
315
    {
316
        // Retrieve the authentication state
317
        if (!$request->query->has('AuthState')) {
318
            throw new Error\BadRequest('Missing AuthState parameter.');
319
        }
320
        $authStateId = $request->query->get('AuthState');
321
        $this->authState::validateStateId($authStateId);
322
323
        $state = $this->authState::loadState($authStateId, UserPassOrgBase::STAGEID);
324
325
        /** @var \SimpleSAML\Module\core\Auth\UserPassOrgBase $source */
326
        $source = $this->authSource::getById($state[UserPassOrgBase::AUTHID]);
327
        if ($source === null) {
328
            throw new BuiltinException(
329
                'Could not find authentication source with id ' . $state[UserPassOrgBase::AUTHID]
330
            );
331
        }
332
333
        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

333
        return $this->handleLogin($request, $source, /** @scrutinizer ignore-type */ $state);
Loading history...
334
    }
335
336
337
    /**
338
     * @param string $name     The name for the cookie
339
     * @param string $value    The value for the cookie
340
     * @param int $expire      The expiration in seconds
341
     * @param string $path     The path for the cookie
342
     * @param string $domain   The domain for the cookie
343
     * @param bool $secure     Whether this cookie must have the secure-flag
344
     * @param bool $httponly   Whether this cookie must have the httponly-flag
345
     * @param bool $raw        Whether this cookie must be sent without urlencoding
346
     * @param string $sameSite The value for the sameSite-flag
347
     * @return \Symfony\Component\HttpFoundation\Cookie
348
     */
349
    private function renderCookie(
350
        string $name,
351
        ?string $value,
352
        int $expire = 0,
353
        string $path = '/',
354
        ?string $domain = null,
355
        ?bool $secure = null,
356
        bool $httponly = true,
357
        bool $raw = false,
358
        ?string $sameSite = 'none'
359
    ): Cookie {
360
        return new Cookie($name, $value, $expire, $path, $domain, $secure, $httponly, $raw, $sameSite);
361
    }
362
363
364
    /**
365
     * Retrieve the username from the request, a cookie or the state
366
     *
367
     * @param \Symfony\Component\HttpFoundation\Request $request
368
     * @param \SimpleSAML\Auth\Source $source
369
     * @param array $state
370
     * @return string
371
     */
372
    private function getUsernameFromRequest(Request $request, Auth\Source $source, array $state): string
373
    {
374
        $username = '';
375
376
        if ($request->request->has('username')) {
377
            $username = trim($request->request->get('username'));
378
        } elseif (
379
            $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

379
            $source->/** @scrutinizer ignore-call */ 
380
                     getRememberUsernameEnabled()
Loading history...
380
            && $request->cookies->has($source->getAuthId() . '-username')
381
        ) {
382
            $username = $request->cookies->get($source->getAuthId() . '-username');
383
        } elseif (isset($state['core:username'])) {
384
            $username = strval($state['core:username']);
385
        }
386
387
        return $username;
388
    }
389
390
391
    /**
392
     * Retrieve the password from the request
393
     *
394
     * @param \Symfony\Component\HttpFoundation\Request $request
395
     * @return string
396
     */
397
    private function getPasswordFromRequest(Request $request): string
398
    {
399
        $password = '';
400
401
        if ($request->request->has('password')) {
402
            $password = $request->request->get('password');
403
        }
404
405
        return $password;
406
    }
407
408
409
    /**
410
     * Retrieve the organization from the request, a cookie or the state
411
     *
412
     * @param \Symfony\Component\HttpFoundation\Request $request
413
     * @param \SimpleSAML\Auth\Source $source
414
     * @param array $state
415
     * @return string
416
     */
417
    private function getOrganizationFromRequest(Request $request, Auth\Source $source, array $state): string
418
    {
419
        $organization = '';
420
421
        if ($request->request->has('organization')) {
422
            $organization = $request->request->get('organization');
423
        } elseif (
424
            $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

424
            $source->/** @scrutinizer ignore-call */ 
425
                     getRememberOrganizationEnabled()
Loading history...
425
            && $request->cookies->has($source->getAuthId() . '-organization')
426
        ) {
427
            $organization = $request->cookies->get($source->getAuthId() . '-organization');
428
        } elseif (isset($state['core:organization'])) {
429
            $organization = strval($state['core:organization']);
430
        }
431
432
        return $organization;
433
    }
434
435
436
    /**
437
     * Searches for a valid and allowed ReturnTo URL parameter,
438
     * otherwise give the base installation page as a return point.
439
     */
440
    private function getReturnPath(Request $request): string
441
    {
442
        $httpUtils = new Utils\HTTP();
443
444
        $returnTo = $request->query->get('ReturnTo', false);
445
        if ($returnTo !== false) {
446
            $returnTo = $httpUtils->checkURLAllowed($returnTo);
447
        }
448
        if (empty($returnTo)) {
449
            return $this->config->getBasePath();
450
        }
451
        return $returnTo;
452
    }
453
454
455
    /**
456
     * This clears the user's IdP discovery choices.
457
     *
458
     * @param Request $request The request that lead to this login operation.
459
     */
460
    public function cleardiscochoices(Request $request): RedirectResponse
461
    {
462
        $httpUtils = new Utils\HTTP();
463
464
        // The base path for cookies. This should be the installation directory for SimpleSAMLphp.
465
        $cookiePath = $this->config->getBasePath();
466
467
        // We delete all cookies which starts with 'idpdisco_'
468
        foreach ($request->cookies->all() as $cookieName => $value) {
469
            if (substr($cookieName, 0, 9) !== 'idpdisco_') {
470
                // Not a idpdisco cookie.
471
                continue;
472
            }
473
474
            $httpUtils->setCookie($cookieName, null, ['path' => $cookiePath, 'httponly' => false], false);
475
        }
476
477
        $returnTo = $this->getReturnPath($request);
478
479
        // Redirect to destination.
480
        return $httpUtils->redirectTrustedURL($returnTo);
481
    }
482
}
483