Completed
Push — release-2.1 ( 121660...f19596 )
by Mathias
09:09
created

Sources/LogInOut.php (16 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * This file is concerned pretty entirely, as you see from its name, with
5
 * logging in and out members, and the validation of that.
6
 *
7
 * Simple Machines Forum (SMF)
8
 *
9
 * @package SMF
10
 * @author Simple Machines http://www.simplemachines.org
11
 * @copyright 2017 Simple Machines and individual contributors
12
 * @license http://www.simplemachines.org/about/smf/license.php BSD
13
 *
14
 * @version 2.1 Beta 4
15
 */
16
17
if (!defined('SMF'))
18
	die('No direct access...');
19
20
/**
21
 * Ask them for their login information. (shows a page for the user to type
22
 *  in their username and password.)
23
 *  It caches the referring URL in $_SESSION['login_url'].
24
 *  It is accessed from ?action=login.
25
 *  @uses Login template and language file with the login sub-template.
26
 */
27
function Login()
28
{
29
	global $txt, $context, $scripturl, $user_info;
30
31
	// You are already logged in, go take a tour of the boards
32
	if (!empty($user_info['id']))
33
		redirectexit();
34
35
	// We need to load the Login template/language file.
36
	loadLanguage('Login');
37
	loadTemplate('Login');
38
39
	$context['sub_template'] = 'login';
40
41 View Code Duplication
	if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
42
	{
43
		$context['from_ajax'] = true;
44
		$context['template_layers'] = array();
45
	}
46
47
	// Get the template ready.... not really much else to do.
48
	$context['page_title'] = $txt['login'];
49
	$context['default_username'] = &$_REQUEST['u'];
50
	$context['default_password'] = '';
51
	$context['never_expire'] = false;
52
53
	// Add the login chain to the link tree.
54
	$context['linktree'][] = array(
55
		'url' => $scripturl . '?action=login',
56
		'name' => $txt['login'],
57
	);
58
59
	// Set the login URL - will be used when the login process is done (but careful not to send us to an attachment).
60
	if (isset($_SESSION['old_url']) && strpos($_SESSION['old_url'], 'dlattach') === false && preg_match('~(board|topic)[=,]~', $_SESSION['old_url']) != 0)
61
		$_SESSION['login_url'] = $_SESSION['old_url'];
62
	elseif (isset($_SESSION['login_url']) && strpos($_SESSION['login_url'], 'dlattach') !== false)
63
		unset($_SESSION['login_url']);
64
65
	// Create a one time token.
66
	createToken('login');
67
}
68
69
/**
70
 * Actually logs you in.
71
 * What it does:
72
 * - checks credentials and checks that login was successful.
73
 * - it employs protection against a specific IP or user trying to brute force
74
 *  a login to an account.
75
 * - upgrades password encryption on login, if necessary.
76
 * - after successful login, redirects you to $_SESSION['login_url'].
77
 * - accessed from ?action=login2, by forms.
78
 * On error, uses the same templates Login() uses.
79
 */
80
function Login2()
81
{
82
	global $txt, $scripturl, $user_info, $user_settings, $smcFunc;
83
	global $cookiename, $modSettings, $context, $sourcedir, $maintenance;
84
85
	// Check to ensure we're forcing SSL for authentication
86 View Code Duplication
	if (!empty($modSettings['force_ssl']) && empty($maintenance) && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on'))
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
87
		fatal_lang_error('login_ssl_required');
88
89
	// Load cookie authentication stuff.
90
	require_once($sourcedir . '/Subs-Auth.php');
91
92 View Code Duplication
	if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
93
	{
94
		$context['from_ajax'] = true;
95
		$context['template_layers'] = array();
96
	}
97
98
	if (isset($_GET['sa']) && $_GET['sa'] == 'salt' && !$user_info['is_guest'])
99
	{
100
		if (isset($_COOKIE[$cookiename]) && preg_match('~^a:[34]:\{i:0;i:\d{1,7};i:1;s:(0|128):"([a-fA-F0-9]{128})?";i:2;[id]:\d{1,14};(i:3;i:\d;)?\}$~', $_COOKIE[$cookiename]) === 1)
101
		{
102
			list (,, $timeout) = $smcFunc['json_decode']($_COOKIE[$cookiename], true);
103
104
			// That didn't work... Maybe it's using serialize?
105
			if (is_null($timeout))
106
				list (,, $timeout) = safe_unserialize($_COOKIE[$cookiename]);
107
		}
108
		elseif (isset($_SESSION['login_' . $cookiename]))
109
		{
110
			list (,, $timeout) = $smcFunc['json_decode']($_SESSION['login_' . $cookiename]);
111
112
			// Try for old format
113
			if (is_null($timeout))
114
				list (,, $timeout) = safe_unserialize($_SESSION['login_' . $cookiename]);
115
		}
116
		else
117
			trigger_error('Login2(): Cannot be logged in without a session or cookie', E_USER_ERROR);
118
119
		$user_settings['password_salt'] = substr(md5(mt_rand()), 0, 4);
120
		updateMemberData($user_info['id'], array('password_salt' => $user_settings['password_salt']));
121
122
		// Preserve the 2FA cookie?
123 View Code Duplication
		if (!empty($modSettings['tfa_mode']) && !empty($_COOKIE[$cookiename . '_tfa']))
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
124
		{
125
			$tfadata = $smcFunc['json_decode']($_COOKIE[$cookiename . '_tfa'], true);
126
127
			list ($tfamember, $tfasecret, $exp, $state, $preserve) = $tfadata;
128
129
			// If we're preserving the cookie, reset it with updated salt
130
			if (isset($tfamember, $tfasecret, $exp, $state, $preserve) && $preserve && time() < $exp)
131
				setTFACookie(3153600, $user_info['password_salt'], hash_salt($user_settings['tfa_backup'], $user_settings['password_salt']), true);
132
			else
133
				setTFACookie(-3600, 0, '');
134
		}
135
136
		setLoginCookie($timeout - time(), $user_info['id'], hash_salt($user_settings['passwd'], $user_settings['password_salt']));
0 ignored issues
show
The variable $timeout does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
137
138
		redirectexit('action=login2;sa=check;member=' . $user_info['id'], $context['server']['needs_login_fix']);
139
	}
140
	// Double check the cookie...
141
	elseif (isset($_GET['sa']) && $_GET['sa'] == 'check')
142
	{
143
		// Strike!  You're outta there!
144
		if ($_GET['member'] != $user_info['id'])
145
			fatal_lang_error('login_cookie_error', false);
146
147
		$user_info['can_mod'] = allowedTo('access_mod_center') || (!$user_info['is_guest'] && ($user_info['mod_cache']['gq'] != '0=1' || $user_info['mod_cache']['bq'] != '0=1' || ($modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']))));
148
149
		// Some whitelisting for login_url...
150
		if (empty($_SESSION['login_url']))
151
			redirectexit(empty($user_settings['tfa_secret']) ? '' : 'action=logintfa');
152
		elseif (!empty($_SESSION['login_url']) && (strpos($_SESSION['login_url'], 'http://') === false && strpos($_SESSION['login_url'], 'https://') === false))
153
		{
154
			unset ($_SESSION['login_url']);
155
			redirectexit(empty($user_settings['tfa_secret']) ? '' : 'action=logintfa');
156
		}
157
		else
158
		{
159
			// Best not to clutter the session data too much...
160
			$temp = $_SESSION['login_url'];
161
			unset($_SESSION['login_url']);
162
163
			redirectexit($temp);
164
		}
165
	}
166
167
	// Beyond this point you are assumed to be a guest trying to login.
168
	if (!$user_info['is_guest'])
169
		redirectexit();
170
171
	// Are you guessing with a script?
172
	checkSession();
173
	validateToken('login');
174
	spamProtection('login');
175
176
	// Set the login_url if it's not already set (but careful not to send us to an attachment).
177
	if ((empty($_SESSION['login_url']) && isset($_SESSION['old_url']) && strpos($_SESSION['old_url'], 'dlattach') === false && preg_match('~(board|topic)[=,]~', $_SESSION['old_url']) != 0) || (isset($_GET['quicklogin']) && isset($_SESSION['old_url']) && strpos($_SESSION['old_url'], 'login') === false))
178
		$_SESSION['login_url'] = $_SESSION['old_url'];
179
180
	// Been guessing a lot, haven't we?
181
	if (isset($_SESSION['failed_login']) && $_SESSION['failed_login'] >= $modSettings['failed_login_threshold'] * 3)
182
		fatal_lang_error('login_threshold_fail', 'critical');
183
184
	// Set up the cookie length.  (if it's invalid, just fall through and use the default.)
185
	if (isset($_POST['cookieneverexp']) || (!empty($_POST['cookielength']) && $_POST['cookielength'] == -1))
186
		$modSettings['cookieTime'] = 3153600;
187
	elseif (!empty($_POST['cookielength']) && ($_POST['cookielength'] >= 1 && $_POST['cookielength'] <= 525600))
188
		$modSettings['cookieTime'] = (int) $_POST['cookielength'];
189
190
	loadLanguage('Login');
191
	// Load the template stuff.
192
	loadTemplate('Login');
193
	$context['sub_template'] = 'login';
194
195
	// Set up the default/fallback stuff.
196
	$context['default_username'] = isset($_POST['user']) ? preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', $smcFunc['htmlspecialchars']($_POST['user'])) : '';
197
	$context['default_password'] = '';
198
	$context['never_expire'] = $modSettings['cookieTime'] == 525600 || $modSettings['cookieTime'] == 3153600;
199
	$context['login_errors'] = array($txt['error_occured']);
200
	$context['page_title'] = $txt['login'];
201
202
	// Add the login chain to the link tree.
203
	$context['linktree'][] = array(
204
		'url' => $scripturl . '?action=login',
205
		'name' => $txt['login'],
206
	);
207
208
	// You forgot to type your username, dummy!
209 View Code Duplication
	if (!isset($_POST['user']) || $_POST['user'] == '')
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
210
	{
211
		$context['login_errors'] = array($txt['need_username']);
212
		return;
213
	}
214
215
	// Hmm... maybe 'admin' will login with no password. Uhh... NO!
216 View Code Duplication
	if (!isset($_POST['passwrd']) || $_POST['passwrd'] == '')
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
217
	{
218
		$context['login_errors'] = array($txt['no_password']);
219
		return;
220
	}
221
222
	// No funky symbols either.
223
	if (preg_match('~[<>&"\'=\\\]~', preg_replace('~(&#(\\d{1,7}|x[0-9a-fA-F]{1,6});)~', '', $_POST['user'])) != 0)
224
	{
225
		$context['login_errors'] = array($txt['error_invalid_characters_username']);
226
		return;
227
	}
228
229
	// And if it's too long, trim it back.
230
	if ($smcFunc['strlen']($_POST['user']) > 80)
231
	{
232
		$_POST['user'] = $smcFunc['substr']($_POST['user'], 0, 79);
233
		$context['default_username'] = preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', $smcFunc['htmlspecialchars']($_POST['user']));
234
	}
235
236
237
	// Are we using any sort of integration to validate the login?
238
	if (in_array('retry', call_integration_hook('integrate_validate_login', array($_POST['user'], isset($_POST['passwrd']) ? $_POST['passwrd'] : null, $modSettings['cookieTime'])), true))
239
	{
240
		$context['login_errors'] = array($txt['incorrect_password']);
241
		return;
242
	}
243
244
	// Load the data up!
245
	$request = $smcFunc['db_query']('', '
246
		SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
247
			passwd_flood, tfa_secret
248
		FROM {db_prefix}members
249
		WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name) = LOWER({string:user_name})' : 'member_name = {string:user_name}') . '
250
		LIMIT 1',
251
		array(
252
			'user_name' => $smcFunc['db_case_sensitive'] ? strtolower($_POST['user']) : $_POST['user'],
253
		)
254
	);
255
	// Probably mistyped or their email, try it as an email address. (member_name first, though!)
256
	if ($smcFunc['db_num_rows']($request) == 0 && strpos($_POST['user'], '@') !== false)
257
	{
258
		$smcFunc['db_free_result']($request);
259
260
		$request = $smcFunc['db_query']('', '
261
			SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
262
			passwd_flood, tfa_secret
263
			FROM {db_prefix}members
264
			WHERE email_address = {string:user_name}
265
			LIMIT 1',
266
			array(
267
				'user_name' => $_POST['user'],
268
			)
269
		);
270
	}
271
272
	// Let them try again, it didn't match anything...
273
	if ($smcFunc['db_num_rows']($request) == 0)
274
	{
275
		$context['login_errors'] = array($txt['username_no_exist']);
276
		return;
277
	}
278
279
	$user_settings = $smcFunc['db_fetch_assoc']($request);
280
	$smcFunc['db_free_result']($request);
281
282
	// Bad password!  Thought you could fool the database?!
283
	if (!hash_verify_password($user_settings['member_name'], un_htmlspecialchars($_POST['passwrd']), $user_settings['passwd']))
284
	{
285
		// Let's be cautious, no hacking please. thanx.
286
		validatePasswordFlood($user_settings['id_member'], $user_settings['passwd_flood']);
287
288
		// Maybe we were too hasty... let's try some other authentication methods.
289
		$other_passwords = array();
290
291
		// None of the below cases will be used most of the time (because the salt is normally set.)
292
		if (!empty($modSettings['enable_password_conversion']) && $user_settings['password_salt'] == '')
293
		{
294
			// YaBB SE, Discus, MD5 (used a lot), SHA-1 (used some), SMF 1.0.x, IkonBoard, and none at all.
295
			$other_passwords[] = crypt($_POST['passwrd'], substr($_POST['passwrd'], 0, 2));
296
			$other_passwords[] = crypt($_POST['passwrd'], substr($user_settings['passwd'], 0, 2));
297
			$other_passwords[] = md5($_POST['passwrd']);
298
			$other_passwords[] = sha1($_POST['passwrd']);
299
			$other_passwords[] = md5_hmac($_POST['passwrd'], strtolower($user_settings['member_name']));
300
			$other_passwords[] = md5($_POST['passwrd'] . strtolower($user_settings['member_name']));
301
			$other_passwords[] = md5(md5($_POST['passwrd']));
302
			$other_passwords[] = $_POST['passwrd'];
303
304
			// This one is a strange one... MyPHP, crypt() on the MD5 hash.
305
			$other_passwords[] = crypt(md5($_POST['passwrd']), md5($_POST['passwrd']));
306
307
			// Snitz style - SHA-256.  Technically, this is a downgrade, but most PHP configurations don't support sha256 anyway.
308
			if (strlen($user_settings['passwd']) == 64 && function_exists('mhash') && defined('MHASH_SHA256'))
309
				$other_passwords[] = bin2hex(mhash(MHASH_SHA256, $_POST['passwrd']));
310
311
			// phpBB3 users new hashing.  We now support it as well ;).
312
			$other_passwords[] = phpBB3_password_check($_POST['passwrd'], $user_settings['passwd']);
313
314
			// APBoard 2 Login Method.
315
			$other_passwords[] = md5(crypt($_POST['passwrd'], 'CRYPT_MD5'));
316
		}
317
		// The hash should be 40 if it's SHA-1, so we're safe with more here too.
318
		elseif (!empty($modSettings['enable_password_conversion']) && strlen($user_settings['passwd']) == 32)
319
		{
320
			// vBulletin 3 style hashing?  Let's welcome them with open arms \o/.
321
			$other_passwords[] = md5(md5($_POST['passwrd']) . stripslashes($user_settings['password_salt']));
322
323
			// Hmm.. p'raps it's Invision 2 style?
324
			$other_passwords[] = md5(md5($user_settings['password_salt']) . md5($_POST['passwrd']));
325
326
			// Some common md5 ones.
327
			$other_passwords[] = md5($user_settings['password_salt'] . $_POST['passwrd']);
328
			$other_passwords[] = md5($_POST['passwrd'] . $user_settings['password_salt']);
329
		}
330
		elseif (strlen($user_settings['passwd']) == 40)
331
		{
332
			// Maybe they are using a hash from before the password fix.
333
			// This is also valid for SMF 1.1 to 2.0 style of hashing, changed to bcrypt in SMF 2.1
334
			$other_passwords[] = sha1(strtolower($user_settings['member_name']) . un_htmlspecialchars($_POST['passwrd']));
335
336
			// BurningBoard3 style of hashing.
337
			if (!empty($modSettings['enable_password_conversion']))
338
				$other_passwords[] = sha1($user_settings['password_salt'] . sha1($user_settings['password_salt'] . sha1($_POST['passwrd'])));
339
340
			// Perhaps we converted to UTF-8 and have a valid password being hashed differently.
341
			if ($context['character_set'] == 'UTF-8' && !empty($modSettings['previousCharacterSet']) && $modSettings['previousCharacterSet'] != 'utf8')
342
			{
343
				// Try iconv first, for no particular reason.
344
				if (function_exists('iconv'))
345
					$other_passwords['iconv'] = sha1(strtolower(iconv('UTF-8', $modSettings['previousCharacterSet'], $user_settings['member_name'])) . un_htmlspecialchars(iconv('UTF-8', $modSettings['previousCharacterSet'], $_POST['passwrd'])));
346
347
				// Say it aint so, iconv failed!
348 View Code Duplication
				if (empty($other_passwords['iconv']) && function_exists('mb_convert_encoding'))
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
349
					$other_passwords[] = sha1(strtolower(mb_convert_encoding($user_settings['member_name'], 'UTF-8', $modSettings['previousCharacterSet'])) . un_htmlspecialchars(mb_convert_encoding($_POST['passwrd'], 'UTF-8', $modSettings['previousCharacterSet'])));
350
			}
351
		}
352
353
		// SMF's sha1 function can give a funny result on Linux (Not our fault!). If we've now got the real one let the old one be valid!
354
		if (stripos(PHP_OS, 'win') !== 0 && strlen($user_settings['passwd']) < hash_length())
355
		{
356
			require_once($sourcedir . '/Subs-Compat.php');
357
			$other_passwords[] = sha1_smf(strtolower($user_settings['member_name']) . un_htmlspecialchars($_POST['passwrd']));
358
		}
359
360
		// Allows mods to easily extend the $other_passwords array
361
		call_integration_hook('integrate_other_passwords', array(&$other_passwords));
362
363
		// Whichever encryption it was using, let's make it use SMF's now ;).
364
		if (in_array($user_settings['passwd'], $other_passwords))
365
		{
366
			$user_settings['passwd'] = hash_password($user_settings['member_name'], un_htmlspecialchars($_POST['passwrd']));
367
			$user_settings['password_salt'] = substr(md5(mt_rand()), 0, 4);
368
369
			// Update the password and set up the hash.
370
			updateMemberData($user_settings['id_member'], array('passwd' => $user_settings['passwd'], 'password_salt' => $user_settings['password_salt'], 'passwd_flood' => ''));
371
		}
372
		// Okay, they for sure didn't enter the password!
373
		else
374
		{
375
			// They've messed up again - keep a count to see if they need a hand.
376
			$_SESSION['failed_login'] = isset($_SESSION['failed_login']) ? ($_SESSION['failed_login'] + 1) : 1;
377
378
			// Hmm... don't remember it, do you?  Here, try the password reminder ;).
379
			if ($_SESSION['failed_login'] >= $modSettings['failed_login_threshold'])
380
				redirectexit('action=reminder');
381
			// We'll give you another chance...
382
			else
383
			{
384
				// Log an error so we know that it didn't go well in the error log.
385
				log_error($txt['incorrect_password'] . ' - <span class="remove">' . $user_settings['member_name'] . '</span>', 'user');
386
387
				$context['login_errors'] = array($txt['incorrect_password']);
388
				return;
389
			}
390
		}
391
	}
392
	elseif (!empty($user_settings['passwd_flood']))
393
	{
394
		// Let's be sure they weren't a little hacker.
395
		validatePasswordFlood($user_settings['id_member'], $user_settings['passwd_flood'], true);
396
397
		// If we got here then we can reset the flood counter.
398
		updateMemberData($user_settings['id_member'], array('passwd_flood' => ''));
399
	}
400
401
	// Correct password, but they've got no salt; fix it!
402
	if ($user_settings['password_salt'] == '')
403
	{
404
		$user_settings['password_salt'] = substr(md5(mt_rand()), 0, 4);
405
		updateMemberData($user_settings['id_member'], array('password_salt' => $user_settings['password_salt']));
406
	}
407
408
	// Check their activation status.
409
	if (!checkActivation())
410
		return;
411
412
	DoLogin();
413
}
414
415
/**
416
 * Allows the user to enter their Two-Factor Authentication code
417
 */
418
function LoginTFA()
419
{
420
	global $sourcedir, $txt, $context, $user_info, $modSettings, $scripturl;
421
422
	if (!$user_info['is_guest'] || empty($context['tfa_member']) || empty($modSettings['tfa_mode']))
423
		fatal_lang_error('no_access', false);
424
425
	loadLanguage('Profile');
426
	require_once($sourcedir . '/Class-TOTP.php');
427
428
	$member = $context['tfa_member'];
429
430
	// Prevent replay attacks by limiting at least 2 minutes before they can log in again via 2FA
431
	if (time() - $member['last_login'] < 120)
432
		fatal_lang_error('tfa_wait', false);
433
434
	$totp = new \TOTP\Auth($member['tfa_secret']);
435
	$totp->setRange(1);
436
437 View Code Duplication
	if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
438
	{
439
		$context['from_ajax'] = true;
440
		$context['template_layers'] = array();
441
	}
442
443
	if (!empty($_POST['tfa_code']) && empty($_POST['tfa_backup']))
444
	{
445
		// Check to ensure we're forcing SSL for authentication
446 View Code Duplication
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on'))
0 ignored issues
show
The variable $maintenance seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
447
			fatal_lang_error('login_ssl_required');
448
449
		$code = $_POST['tfa_code'];
450
451
		if (strlen($code) == $totp->getCodeLength() && $totp->validateCode($code))
452
		{
453
			updateMemberData($member['id_member'], array('last_login' => time()));
454
455
			setTFACookie(3153600, $member['id_member'], hash_salt($member['tfa_backup'], $member['password_salt']), !empty($_POST['tfa_preserve']));
456
			redirectexit();
457
		}
458
		else
459
		{
460
			validatePasswordFlood($member['id_member'], $member['passwd_flood'], false, true);
461
462
			$context['tfa_error'] = true;
463
			$context['tfa_value'] = $_POST['tfa_code'];
464
		}
465
	}
466
	elseif (!empty($_POST['tfa_backup']))
467
	{
468
		// Check to ensure we're forcing SSL for authentication
469 View Code Duplication
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on'))
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
470
			fatal_lang_error('login_ssl_required');
471
472
		$backup = $_POST['tfa_backup'];
473
474
		if (hash_verify_password($member['member_name'], $backup, $member['tfa_backup']))
475
		{
476
			// Get rid of their current TFA settings
477
			updateMemberData($member['id_member'], array(
478
				'tfa_secret' => '',
479
				'tfa_backup' => '',
480
				'last_login' => time(),
481
			));
482
			setTFACookie(3153600, $member['id_member'], hash_salt($member['tfa_backup'], $member['password_salt']));
483
			redirectexit('action=profile;area=tfasetup;backup');
484
		}
485
		else
486
		{
487
			validatePasswordFlood($member['id_member'], $member['passwd_flood'], false, true);
488
489
			$context['tfa_backup_error'] = true;
490
			$context['tfa_value'] = $_POST['tfa_code'];
491
			$context['tfa_backup_value'] = $_POST['tfa_backup'];
492
		}
493
	}
494
495
	loadTemplate('Login');
496
	$context['sub_template'] = 'login_tfa';
497
	$context['page_title'] = $txt['login'];
498
	$context['tfa_url'] = (!empty($modSettings['force_ssl']) && $modSettings['force_ssl'] < 2 ? strtr($scripturl, array('http://' => 'https://')) : $scripturl) . '?action=logintfa';
499
}
500
501
/**
502
 * Check activation status of the current user.
503
 */
504
function checkActivation()
505
{
506
	global $context, $txt, $scripturl, $user_settings, $modSettings;
507
508
	if (!isset($context['login_errors']))
509
		$context['login_errors'] = array();
510
511
	// What is the true activation status of this account?
512
	$activation_status = $user_settings['is_activated'] > 10 ? $user_settings['is_activated'] - 10 : $user_settings['is_activated'];
513
514
	// Check if the account is activated - COPPA first...
515
	if ($activation_status == 5)
516
	{
517
		$context['login_errors'][] = $txt['coppa_no_concent'] . ' <a href="' . $scripturl . '?action=coppa;member=' . $user_settings['id_member'] . '">' . $txt['coppa_need_more_details'] . '</a>';
518
		return false;
519
	}
520
	// Awaiting approval still?
521
	elseif ($activation_status == 3)
522
		fatal_lang_error('still_awaiting_approval', 'user');
523
	// Awaiting deletion, changed their mind?
524
	elseif ($activation_status == 4)
525
	{
526
		if (isset($_REQUEST['undelete']))
527
		{
528
			updateMemberData($user_settings['id_member'], array('is_activated' => 1));
529
			updateSettings(array('unapprovedMembers' => ($modSettings['unapprovedMembers'] > 0 ? $modSettings['unapprovedMembers'] - 1 : 0)));
530
		}
531
		else
532
		{
533
			$context['disable_login_hashing'] = true;
534
			$context['login_errors'][] = $txt['awaiting_delete_account'];
535
			$context['login_show_undelete'] = true;
536
			return false;
537
		}
538
	}
539
	// Standard activation?
540
	elseif ($activation_status != 1)
541
	{
542
		log_error($txt['activate_not_completed1'] . ' - <span class="remove">' . $user_settings['member_name'] . '</span>', false);
0 ignored issues
show
false is of type boolean, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
543
544
		$context['login_errors'][] = $txt['activate_not_completed1'] . ' <a href="' . $scripturl . '?action=activate;sa=resend;u=' . $user_settings['id_member'] . '">' . $txt['activate_not_completed2'] . '</a>';
545
		return false;
546
	}
547
	return true;
548
}
549
550
/**
551
 * Perform the logging in. (set cookie, call hooks, etc)
552
 */
553
function DoLogin()
554
{
555
	global $user_info, $user_settings, $smcFunc;
556
	global $maintenance, $modSettings, $context, $sourcedir;
557
558
	// Load cookie authentication stuff.
559
	require_once($sourcedir . '/Subs-Auth.php');
560
561
	// Call login integration functions.
562
	call_integration_hook('integrate_login', array($user_settings['member_name'], null, $modSettings['cookieTime']));
563
564
	// Get ready to set the cookie...
565
	$user_info['id'] = $user_settings['id_member'];
566
567
	// Bam!  Cookie set.  A session too, just in case.
568
	setLoginCookie(60 * $modSettings['cookieTime'], $user_settings['id_member'], hash_salt($user_settings['passwd'], $user_settings['password_salt']));
569
570
	// Reset the login threshold.
571
	if (isset($_SESSION['failed_login']))
572
		unset($_SESSION['failed_login']);
573
574
	$user_info['is_guest'] = false;
575
	$user_settings['additional_groups'] = explode(',', $user_settings['additional_groups']);
576
	$user_info['is_admin'] = $user_settings['id_group'] == 1 || in_array(1, $user_settings['additional_groups']);
577
578
	// Are you banned?
579
	is_not_banned(true);
580
581
	// Don't stick the language or theme after this point.
582
	unset($_SESSION['language'], $_SESSION['id_theme']);
583
584
	// First login?
585
	$request = $smcFunc['db_query']('', '
586
		SELECT last_login
587
		FROM {db_prefix}members
588
		WHERE id_member = {int:id_member}
589
			AND last_login = 0',
590
		array(
591
			'id_member' => $user_info['id'],
592
		)
593
	);
594
	if ($smcFunc['db_num_rows']($request) == 1)
595
		$_SESSION['first_login'] = true;
596
	else
597
		unset($_SESSION['first_login']);
598
	$smcFunc['db_free_result']($request);
599
600
	// You've logged in, haven't you?
601
	$update = array('member_ip' => $user_info['ip'], 'member_ip2' => $_SERVER['BAN_CHECK_IP']);
602
	if (empty($user_settings['tfa_secret']))
603
		$update['last_login'] = time();
604
	updateMemberData($user_info['id'], $update);
605
606
	// Get rid of the online entry for that old guest....
607
	$smcFunc['db_query']('', '
608
		DELETE FROM {db_prefix}log_online
609
		WHERE session = {string:session}',
610
		array(
611
			'session' => 'ip' . $user_info['ip'],
612
		)
613
	);
614
	$_SESSION['log_time'] = 0;
615
616
	// Log this entry, only if we have it enabled.
617
	if (!empty($modSettings['loginHistoryDays']))
618
		$smcFunc['db_insert']('insert',
619
			'{db_prefix}member_logins',
620
			array(
621
				'id_member' => 'int', 'time' => 'int', 'ip' => 'inet', 'ip2' => 'inet',
622
			),
623
			array(
624
				$user_info['id'], time(), $user_info['ip'], $user_info['ip2']
625
			),
626
			array(
627
				'id_member', 'time'
628
			)
629
		);
630
631
	// Just log you back out if it's in maintenance mode and you AREN'T an admin.
632
	if (empty($maintenance) || allowedTo('admin_forum'))
633
		redirectexit('action=login2;sa=check;member=' . $user_info['id'], $context['server']['needs_login_fix']);
634
	else
635
		redirectexit('action=logout;' . $context['session_var'] . '=' . $context['session_id'], $context['server']['needs_login_fix']);
636
}
637
638
/**
639
 * Logs the current user out of their account.
640
 * It requires that the session hash is sent as well, to prevent automatic logouts by images or javascript.
641
 * It redirects back to $_SESSION['logout_url'], if it exists.
642
 * It is accessed via ?action=logout;session_var=...
643
 *
644
 * @param bool $internal If true, it doesn't check the session
645
 * @param bool $redirect Whether or not to redirect the user after they log out
646
 */
647
function Logout($internal = false, $redirect = true)
648
{
649
	global $sourcedir, $user_info, $user_settings, $context, $smcFunc, $cookiename, $modSettings;
650
651
	// Make sure they aren't being auto-logged out.
652
	if (!$internal)
653
		checkSession('get');
654
655
	require_once($sourcedir . '/Subs-Auth.php');
656
657
	if (isset($_SESSION['pack_ftp']))
658
		$_SESSION['pack_ftp'] = null;
659
660
	// It won't be first login anymore.
661
	unset($_SESSION['first_login']);
662
663
	// Just ensure they aren't a guest!
664
	if (!$user_info['is_guest'])
665
	{
666
		// Pass the logout information to integrations.
667
		call_integration_hook('integrate_logout', array($user_settings['member_name']));
668
669
		// If you log out, you aren't online anymore :P.
670
		$smcFunc['db_query']('', '
671
			DELETE FROM {db_prefix}log_online
672
			WHERE id_member = {int:current_member}',
673
			array(
674
				'current_member' => $user_info['id'],
675
			)
676
		);
677
	}
678
679
	$_SESSION['log_time'] = 0;
680
681
	// Empty the cookie! (set it in the past, and for id_member = 0)
682
	setLoginCookie(-3600, 0);
683
684
	// And some other housekeeping while we're at it.
685
	$salt = substr(md5(mt_rand()), 0, 4);
686
	if (!empty($user_info['id']))
687
		updateMemberData($user_info['id'], array('password_salt' => $salt));
688
689 View Code Duplication
	if (!empty($modSettings['tfa_mode']) && !empty($user_info['id']) && !empty($_COOKIE[$cookiename . '_tfa']))
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
690
	{
691
		$tfadata = smf_json_decode($_COOKIE[$cookiename . '_tfa'], true);
692
693
		list ($tfamember, $tfasecret, $exp, $state, $preserve) = $tfadata;
694
695
		// If we're preserving the cookie, reset it with updated salt
696
		if (isset($tfamember, $tfasecret, $exp, $state, $preserve) && $preserve && time() < $exp)
697
			setTFACookie(3153600, $user_info['id'], hash_salt($user_settings['tfa_backup'], $salt), true);
698
		else
699
			setTFACookie(-3600, 0, '');
700
	}
701
702
	session_destroy();
703
704
	// Off to the merry board index we go!
705
	if ($redirect)
706
	{
707
		if (empty($_SESSION['logout_url']))
708
			redirectexit('', $context['server']['needs_login_fix']);
709
		elseif (!empty($_SESSION['logout_url']) && (strpos($_SESSION['logout_url'], 'http://') === false && strpos($_SESSION['logout_url'], 'https://') === false))
710
		{
711
			unset ($_SESSION['logout_url']);
712
			redirectexit();
713
		}
714
		else
715
		{
716
			$temp = $_SESSION['logout_url'];
717
			unset($_SESSION['logout_url']);
718
719
			redirectexit($temp, $context['server']['needs_login_fix']);
720
		}
721
	}
722
}
723
724
/**
725
 * MD5 Encryption used for older passwords. (SMF 1.0.x/YaBB SE 1.5.x hashing)
726
 *
727
 * @param string $data The data
728
 * @param string $key The key
729
 * @return string The HMAC MD5 of data with key
730
 */
731
function md5_hmac($data, $key)
732
{
733
	$key = str_pad(strlen($key) <= 64 ? $key : pack('H*', md5($key)), 64, chr(0x00));
734
	return md5(($key ^ str_repeat(chr(0x5c), 64)) . pack('H*', md5(($key ^ str_repeat(chr(0x36), 64)) . $data)));
735
}
736
737
/**
738
 * Custom encryption for phpBB3 based passwords.
739
 *
740
 * @param string $passwd The raw (unhashed) password
741
 * @param string $passwd_hash The hashed password
742
 * @return string The hashed version of $passwd
743
 */
744
function phpBB3_password_check($passwd, $passwd_hash)
745
{
746
	// Too long or too short?
747
	if (strlen($passwd_hash) != 34)
748
		return;
749
750
	// Range of characters allowed.
751
	$range = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
752
753
	// Tests
754
	$strpos = strpos($range, $passwd_hash[3]);
755
	$count = 1 << $strpos;
756
	$salt = substr($passwd_hash, 4, 8);
757
758
	$hash = md5($salt . $passwd, true);
759
	for (; $count != 0; --$count)
760
		$hash = md5($hash . $passwd, true);
761
762
	$output = substr($passwd_hash, 0, 12);
763
	$i = 0;
764
	while ($i < 16)
765
	{
766
		$value = ord($hash[$i++]);
767
		$output .= $range[$value & 0x3f];
768
769
		if ($i < 16)
770
			$value |= ord($hash[$i]) << 8;
771
772
		$output .= $range[($value >> 6) & 0x3f];
773
774
		if ($i++ >= 16)
775
			break;
776
777
		if ($i < 16)
778
			$value |= ord($hash[$i]) << 16;
779
780
		$output .= $range[($value >> 12) & 0x3f];
781
782
		if ($i++ >= 16)
783
			break;
784
785
		$output .= $range[($value >> 18) & 0x3f];
786
	}
787
788
	// Return now.
789
	return $output;
790
}
791
792
/**
793
 * This protects against brute force attacks on a member's password.
794
 * Importantly, even if the password was right we DON'T TELL THEM!
795
 *
796
 * @param int $id_member The ID of the member
797
 * @param bool|string $password_flood_value False if we don't have a flood value, otherwise a string with a timestamp and number of tries separated by a |
798
 * @param bool $was_correct Whether or not the password was correct
799
 * @param bool $tfa Whether we're validating for two-factor authentication
800
 */
801
function validatePasswordFlood($id_member, $password_flood_value = false, $was_correct = false, $tfa = false)
802
{
803
	global $cookiename, $sourcedir;
804
805
	// As this is only brute protection, we allow 5 attempts every 10 seconds.
806
807
	// Destroy any session or cookie data about this member, as they validated wrong.
808
	// Only if they're not validating for 2FA
809
	if (!$tfa)
810
	{
811
		require_once($sourcedir . '/Subs-Auth.php');
812
		setLoginCookie(-3600, 0);
813
814
		if (isset($_SESSION['login_' . $cookiename]))
815
			unset($_SESSION['login_' . $cookiename]);
816
	}
817
818
	// We need a member!
819
	if (!$id_member)
820
	{
821
		// Redirect back!
822
		redirectexit();
823
824
		// Probably not needed, but still make sure...
825
		fatal_lang_error('no_access', false);
826
	}
827
828
	// Right, have we got a flood value?
829
	if ($password_flood_value !== false)
830
		@list ($time_stamp, $number_tries) = explode('|', $password_flood_value);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
831
832
	// Timestamp or number of tries invalid?
833
	if (empty($number_tries) || empty($time_stamp))
834
	{
835
		$number_tries = 0;
836
		$time_stamp = time();
837
	}
838
839
	// They've failed logging in already
840
	if (!empty($number_tries))
841
	{
842
		// Give them less chances if they failed before
843
		$number_tries = $time_stamp < time() - 20 ? 2 : $number_tries;
844
845
		// They are trying too fast, make them wait longer
846
		if ($time_stamp < time() - 10)
847
			$time_stamp = time();
848
	}
849
850
	$number_tries++;
851
852
	// Broken the law?
853
	if ($number_tries > 5)
854
		fatal_lang_error('login_threshold_brute_fail', 'critical');
855
856
	// Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it!
857
	updateMemberData($id_member, array('passwd_flood' => $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries));
858
859
}
860
861
?>
0 ignored issues
show
It is not recommended to use PHP's closing tag ?> in files other than templates.

Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.

A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.

Loading history...