Completed
Push — release-2.1 ( aa21c4...7040ad )
by Mathias
09:20
created

LogInOut.php ➔ checkActivation()   D

Complexity

Conditions 9
Paths 24

Size

Total Lines 45
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 24
nc 24
nop 0
dl 0
loc 45
rs 4.909
c 0
b 0
f 0
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
Duplication introduced by
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
Duplication introduced by
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
Duplication introduced by
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) = smf_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) = smf_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
Duplication introduced by
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 = smf_json_decode($_COOKIE[$cookiename . '_tfa'], true);
126
127
			// If that didn't work, try unserialize instead...
128
			if (is_null($tfadata))
129
				$tfadata = safe_unserialize($_COOKIE[$cookiename . '_tfa']);
130
131
			list ($tfamember, $tfasecret, $exp, $state, $preserve) = $tfadata;
0 ignored issues
show
Unused Code introduced by
The assignment to $tfamember is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $tfasecret is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $state is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
132
133
			// If we're preserving the cookie, reset it with updated salt
134
			if ($preserve && time() < $exp)
135
				setTFACookie(3153600, $user_info['password_salt'], hash_salt($user_settings['tfa_backup'], $user_settings['password_salt']), true);
136
			else
137
				setTFACookie(-3600, 0, '');
138
		}
139
140
		setLoginCookie($timeout - time(), $user_info['id'], hash_salt($user_settings['passwd'], $user_settings['password_salt']));
0 ignored issues
show
Bug introduced by
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...
141
142
		redirectexit('action=login2;sa=check;member=' . $user_info['id'], $context['server']['needs_login_fix']);
143
	}
144
	// Double check the cookie...
145
	elseif (isset($_GET['sa']) && $_GET['sa'] == 'check')
146
	{
147
		// Strike!  You're outta there!
148
		if ($_GET['member'] != $user_info['id'])
149
			fatal_lang_error('login_cookie_error', false);
150
151
		$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']))));
152
153
		// Some whitelisting for login_url...
154
		if (empty($_SESSION['login_url']))
155
			redirectexit(empty($user_settings['tfa_secret']) ? '' : 'action=logintfa');
156
		elseif (!empty($_SESSION['login_url']) && (strpos($_SESSION['login_url'], 'http://') === false && strpos($_SESSION['login_url'], 'https://') === false))
157
		{
158
			unset ($_SESSION['login_url']);
159
			redirectexit(empty($user_settings['tfa_secret']) ? '' : 'action=logintfa');
160
		}
161
		else
162
		{
163
			// Best not to clutter the session data too much...
164
			$temp = $_SESSION['login_url'];
165
			unset($_SESSION['login_url']);
166
167
			redirectexit($temp);
168
		}
169
	}
170
171
	// Beyond this point you are assumed to be a guest trying to login.
172
	if (!$user_info['is_guest'])
173
		redirectexit();
174
175
	// Are you guessing with a script?
176
	checkSession();
177
	validateToken('login');
178
	spamProtection('login');
179
180
	// Set the login_url if it's not already set (but careful not to send us to an attachment).
181
	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))
182
		$_SESSION['login_url'] = $_SESSION['old_url'];
183
184
	// Been guessing a lot, haven't we?
185
	if (isset($_SESSION['failed_login']) && $_SESSION['failed_login'] >= $modSettings['failed_login_threshold'] * 3)
186
		fatal_lang_error('login_threshold_fail', 'critical');
187
188
	// Set up the cookie length.  (if it's invalid, just fall through and use the default.)
189
	if (isset($_POST['cookieneverexp']) || (!empty($_POST['cookielength']) && $_POST['cookielength'] == -1))
190
		$modSettings['cookieTime'] = 3153600;
191
	elseif (!empty($_POST['cookielength']) && ($_POST['cookielength'] >= 1 && $_POST['cookielength'] <= 525600))
192
		$modSettings['cookieTime'] = (int) $_POST['cookielength'];
193
194
	loadLanguage('Login');
195
	// Load the template stuff.
196
	loadTemplate('Login');
197
	$context['sub_template'] = 'login';
198
199
	// Set up the default/fallback stuff.
200
	$context['default_username'] = isset($_POST['user']) ? preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', $smcFunc['htmlspecialchars']($_POST['user'])) : '';
201
	$context['default_password'] = '';
202
	$context['never_expire'] = $modSettings['cookieTime'] == 525600 || $modSettings['cookieTime'] == 3153600;
203
	$context['login_errors'] = array($txt['error_occured']);
204
	$context['page_title'] = $txt['login'];
205
206
	// Add the login chain to the link tree.
207
	$context['linktree'][] = array(
208
		'url' => $scripturl . '?action=login',
209
		'name' => $txt['login'],
210
	);
211
212
	// You forgot to type your username, dummy!
213 View Code Duplication
	if (!isset($_POST['user']) || $_POST['user'] == '')
0 ignored issues
show
Duplication introduced by
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...
214
	{
215
		$context['login_errors'] = array($txt['need_username']);
216
		return;
217
	}
218
219
	// Hmm... maybe 'admin' will login with no password. Uhh... NO!
220 View Code Duplication
	if (!isset($_POST['passwrd']) || $_POST['passwrd'] == '')
0 ignored issues
show
Duplication introduced by
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...
221
	{
222
		$context['login_errors'] = array($txt['no_password']);
223
		return;
224
	}
225
226
	// No funky symbols either.
227
	if (preg_match('~[<>&"\'=\\\]~', preg_replace('~(&#(\\d{1,7}|x[0-9a-fA-F]{1,6});)~', '', $_POST['user'])) != 0)
228
	{
229
		$context['login_errors'] = array($txt['error_invalid_characters_username']);
230
		return;
231
	}
232
233
	// And if it's too long, trim it back.
234
	if ($smcFunc['strlen']($_POST['user']) > 80)
235
	{
236
		$_POST['user'] = $smcFunc['substr']($_POST['user'], 0, 79);
237
		$context['default_username'] = preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', $smcFunc['htmlspecialchars']($_POST['user']));
238
	}
239
240
241
	// Are we using any sort of integration to validate the login?
242
	if (in_array('retry', call_integration_hook('integrate_validate_login', array($_POST['user'], isset($_POST['passwrd']) ? $_POST['passwrd'] : null, $modSettings['cookieTime'])), true))
243
	{
244
		$context['login_errors'] = array($txt['incorrect_password']);
245
		return;
246
	}
247
248
	// Load the data up!
249
	$request = $smcFunc['db_query']('', '
250
		SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
251
			passwd_flood, tfa_secret
252
		FROM {db_prefix}members
253
		WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name) = LOWER({string:user_name})' : 'member_name = {string:user_name}') . '
254
		LIMIT 1',
255
		array(
256
			'user_name' => $smcFunc['db_case_sensitive'] ? strtolower($_POST['user']) : $_POST['user'],
257
		)
258
	);
259
	// Probably mistyped or their email, try it as an email address. (member_name first, though!)
260
	if ($smcFunc['db_num_rows']($request) == 0 && strpos($_POST['user'], '@') !== false)
261
	{
262
		$smcFunc['db_free_result']($request);
263
264
		$request = $smcFunc['db_query']('', '
265
			SELECT passwd, id_member, id_group, lngfile, is_activated, email_address, additional_groups, member_name, password_salt,
266
			passwd_flood, tfa_secret
267
			FROM {db_prefix}members
268
			WHERE email_address = {string:user_name}
269
			LIMIT 1',
270
			array(
271
				'user_name' => $_POST['user'],
272
			)
273
		);
274
	}
275
276
	// Let them try again, it didn't match anything...
277
	if ($smcFunc['db_num_rows']($request) == 0)
278
	{
279
		$context['login_errors'] = array($txt['username_no_exist']);
280
		return;
281
	}
282
283
	$user_settings = $smcFunc['db_fetch_assoc']($request);
284
	$smcFunc['db_free_result']($request);
285
286
	// Bad password!  Thought you could fool the database?!
287
	if (!hash_verify_password($user_settings['member_name'], un_htmlspecialchars($_POST['passwrd']), $user_settings['passwd']))
288
	{
289
		// Let's be cautious, no hacking please. thanx.
290
		validatePasswordFlood($user_settings['id_member'], $user_settings['passwd_flood']);
291
292
		// Maybe we were too hasty... let's try some other authentication methods.
293
		$other_passwords = array();
294
295
		// None of the below cases will be used most of the time (because the salt is normally set.)
296
		if (!empty($modSettings['enable_password_conversion']) && $user_settings['password_salt'] == '')
297
		{
298
			// YaBB SE, Discus, MD5 (used a lot), SHA-1 (used some), SMF 1.0.x, IkonBoard, and none at all.
299
			$other_passwords[] = crypt($_POST['passwrd'], substr($_POST['passwrd'], 0, 2));
300
			$other_passwords[] = crypt($_POST['passwrd'], substr($user_settings['passwd'], 0, 2));
301
			$other_passwords[] = md5($_POST['passwrd']);
302
			$other_passwords[] = sha1($_POST['passwrd']);
303
			$other_passwords[] = md5_hmac($_POST['passwrd'], strtolower($user_settings['member_name']));
304
			$other_passwords[] = md5($_POST['passwrd'] . strtolower($user_settings['member_name']));
305
			$other_passwords[] = md5(md5($_POST['passwrd']));
306
			$other_passwords[] = $_POST['passwrd'];
307
308
			// This one is a strange one... MyPHP, crypt() on the MD5 hash.
309
			$other_passwords[] = crypt(md5($_POST['passwrd']), md5($_POST['passwrd']));
310
311
			// Snitz style - SHA-256.  Technically, this is a downgrade, but most PHP configurations don't support sha256 anyway.
312
			if (strlen($user_settings['passwd']) == 64 && function_exists('mhash') && defined('MHASH_SHA256'))
313
				$other_passwords[] = bin2hex(mhash(MHASH_SHA256, $_POST['passwrd']));
314
315
			// phpBB3 users new hashing.  We now support it as well ;).
316
			$other_passwords[] = phpBB3_password_check($_POST['passwrd'], $user_settings['passwd']);
317
318
			// APBoard 2 Login Method.
319
			$other_passwords[] = md5(crypt($_POST['passwrd'], 'CRYPT_MD5'));
320
		}
321
		// The hash should be 40 if it's SHA-1, so we're safe with more here too.
322
		elseif (!empty($modSettings['enable_password_conversion']) && strlen($user_settings['passwd']) == 32)
323
		{
324
			// vBulletin 3 style hashing?  Let's welcome them with open arms \o/.
325
			$other_passwords[] = md5(md5($_POST['passwrd']) . stripslashes($user_settings['password_salt']));
326
327
			// Hmm.. p'raps it's Invision 2 style?
328
			$other_passwords[] = md5(md5($user_settings['password_salt']) . md5($_POST['passwrd']));
329
330
			// Some common md5 ones.
331
			$other_passwords[] = md5($user_settings['password_salt'] . $_POST['passwrd']);
332
			$other_passwords[] = md5($_POST['passwrd'] . $user_settings['password_salt']);
333
		}
334
		elseif (strlen($user_settings['passwd']) == 40)
335
		{
336
			// Maybe they are using a hash from before the password fix.
337
			// This is also valid for SMF 1.1 to 2.0 style of hashing, changed to bcrypt in SMF 2.1
338
			$other_passwords[] = sha1(strtolower($user_settings['member_name']) . un_htmlspecialchars($_POST['passwrd']));
339
340
			// BurningBoard3 style of hashing.
341
			if (!empty($modSettings['enable_password_conversion']))
342
				$other_passwords[] = sha1($user_settings['password_salt'] . sha1($user_settings['password_salt'] . sha1($_POST['passwrd'])));
343
344
			// Perhaps we converted to UTF-8 and have a valid password being hashed differently.
345
			if ($context['character_set'] == 'UTF-8' && !empty($modSettings['previousCharacterSet']) && $modSettings['previousCharacterSet'] != 'utf8')
346
			{
347
				// Try iconv first, for no particular reason.
348
				if (function_exists('iconv'))
349
					$other_passwords['iconv'] = sha1(strtolower(iconv('UTF-8', $modSettings['previousCharacterSet'], $user_settings['member_name'])) . un_htmlspecialchars(iconv('UTF-8', $modSettings['previousCharacterSet'], $_POST['passwrd'])));
350
351
				// Say it aint so, iconv failed!
352 View Code Duplication
				if (empty($other_passwords['iconv']) && function_exists('mb_convert_encoding'))
0 ignored issues
show
Duplication introduced by
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...
353
					$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'])));
354
			}
355
		}
356
357
		// 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!
358
		if (stripos(PHP_OS, 'win') !== 0 && strlen($user_settings['passwd']) < hash_length())
359
		{
360
			require_once($sourcedir . '/Subs-Compat.php');
361
			$other_passwords[] = sha1_smf(strtolower($user_settings['member_name']) . un_htmlspecialchars($_POST['passwrd']));
362
		}
363
364
		// Allows mods to easily extend the $other_passwords array
365
		call_integration_hook('integrate_other_passwords', array(&$other_passwords));
366
367
		// Whichever encryption it was using, let's make it use SMF's now ;).
368
		if (in_array($user_settings['passwd'], $other_passwords))
369
		{
370
			$user_settings['passwd'] = hash_password($user_settings['member_name'], un_htmlspecialchars($_POST['passwrd']));
371
			$user_settings['password_salt'] = substr(md5(mt_rand()), 0, 4);
372
373
			// Update the password and set up the hash.
374
			updateMemberData($user_settings['id_member'], array('passwd' => $user_settings['passwd'], 'password_salt' => $user_settings['password_salt'], 'passwd_flood' => ''));
375
		}
376
		// Okay, they for sure didn't enter the password!
377
		else
378
		{
379
			// They've messed up again - keep a count to see if they need a hand.
380
			$_SESSION['failed_login'] = isset($_SESSION['failed_login']) ? ($_SESSION['failed_login'] + 1) : 1;
381
382
			// Hmm... don't remember it, do you?  Here, try the password reminder ;).
383
			if ($_SESSION['failed_login'] >= $modSettings['failed_login_threshold'])
384
				redirectexit('action=reminder');
385
			// We'll give you another chance...
386
			else
387
			{
388
				// Log an error so we know that it didn't go well in the error log.
389
				log_error($txt['incorrect_password'] . ' - <span class="remove">' . $user_settings['member_name'] . '</span>', 'user');
390
391
				$context['login_errors'] = array($txt['incorrect_password']);
392
				return;
393
			}
394
		}
395
	}
396
	elseif (!empty($user_settings['passwd_flood']))
397
	{
398
		// Let's be sure they weren't a little hacker.
399
		validatePasswordFlood($user_settings['id_member'], $user_settings['passwd_flood'], true);
400
401
		// If we got here then we can reset the flood counter.
402
		updateMemberData($user_settings['id_member'], array('passwd_flood' => ''));
403
	}
404
405
	// Correct password, but they've got no salt; fix it!
406
	if ($user_settings['password_salt'] == '')
407
	{
408
		$user_settings['password_salt'] = substr(md5(mt_rand()), 0, 4);
409
		updateMemberData($user_settings['id_member'], array('password_salt' => $user_settings['password_salt']));
410
	}
411
412
	// Check their activation status.
413
	if (!checkActivation())
414
		return;
415
416
	DoLogin();
417
}
418
419
/**
420
 * Allows the user to enter their Two-Factor Authentication code
421
 */
422
function LoginTFA()
423
{
424
	global $sourcedir, $txt, $context, $user_info, $modSettings, $scripturl;
425
426
	if (!$user_info['is_guest'] || empty($context['tfa_member']) || empty($modSettings['tfa_mode']))
427
		fatal_lang_error('no_access', false);
428
429
	loadLanguage('Profile');
430
	require_once($sourcedir . '/Class-TOTP.php');
431
432
	$member = $context['tfa_member'];
433
434
	// Prevent replay attacks by limiting at least 2 minutes before they can log in again via 2FA
435
	if (time() - $member['last_login'] < 120)
436
		fatal_lang_error('tfa_wait', false);
437
438
	$totp = new \TOTP\Auth($member['tfa_secret']);
439
	$totp->setRange(1);
440
441 View Code Duplication
	if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')
0 ignored issues
show
Duplication introduced by
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...
442
	{
443
		$context['from_ajax'] = true;
444
		$context['template_layers'] = array();
445
	}
446
447
	if (!empty($_POST['tfa_code']) && empty($_POST['tfa_backup']))
448
	{
449
		// Check to ensure we're forcing SSL for authentication
450 View Code Duplication
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on'))
0 ignored issues
show
Bug introduced by
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...
Duplication introduced by
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...
451
			fatal_lang_error('login_ssl_required');
452
453
		$code = $_POST['tfa_code'];
454
455
		if (strlen($code) == $totp->getCodeLength() && $totp->validateCode($code))
456
		{
457
			updateMemberData($member['id_member'], array('last_login' => time()));
458
459
			setTFACookie(3153600, $member['id_member'], hash_salt($member['tfa_backup'], $member['password_salt']), !empty($_POST['tfa_preserve']));
460
			redirectexit();
461
		}
462
		else
463
		{
464
			validatePasswordFlood($member['id_member'], $member['passwd_flood'], false, true);
465
466
			$context['tfa_error'] = true;
467
			$context['tfa_value'] = $_POST['tfa_code'];
468
		}
469
	}
470
	elseif (!empty($_POST['tfa_backup']))
471
	{
472
		// Check to ensure we're forcing SSL for authentication
473 View Code Duplication
		if (!empty($modSettings['force_ssl']) && empty($maintenance) && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on'))
0 ignored issues
show
Duplication introduced by
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...
474
			fatal_lang_error('login_ssl_required');
475
476
		$backup = $_POST['tfa_backup'];
477
478
		if (hash_verify_password($member['member_name'], $backup, $member['tfa_backup']))
479
		{
480
			// Get rid of their current TFA settings
481
			updateMemberData($member['id_member'], array(
482
				'tfa_secret' => '',
483
				'tfa_backup' => '',
484
				'last_login' => time(),
485
			));
486
			setTFACookie(3153600, $member['id_member'], hash_salt($member['tfa_backup'], $member['password_salt']));
487
			redirectexit('action=profile;area=tfasetup;backup');
488
		}
489
		else
490
		{
491
			validatePasswordFlood($member['id_member'], $member['passwd_flood'], false, true);
492
493
			$context['tfa_backup_error'] = true;
494
			$context['tfa_value'] = $_POST['tfa_code'];
495
			$context['tfa_backup_value'] = $_POST['tfa_backup'];
496
		}
497
	}
498
499
	loadTemplate('Login');
500
	$context['sub_template'] = 'login_tfa';
501
	$context['page_title'] = $txt['login'];
502
	$context['tfa_url'] = (!empty($modSettings['force_ssl']) && $modSettings['force_ssl'] < 2 ? strtr($scripturl, array('http://' => 'https://')) : $scripturl) . '?action=logintfa';
503
}
504
505
/**
506
 * Check activation status of the current user.
507
 */
508
function checkActivation()
509
{
510
	global $context, $txt, $scripturl, $user_settings, $modSettings;
511
512
	if (!isset($context['login_errors']))
513
		$context['login_errors'] = array();
514
515
	// What is the true activation status of this account?
516
	$activation_status = $user_settings['is_activated'] > 10 ? $user_settings['is_activated'] - 10 : $user_settings['is_activated'];
517
518
	// Check if the account is activated - COPPA first...
519
	if ($activation_status == 5)
520
	{
521
		$context['login_errors'][] = $txt['coppa_no_concent'] . ' <a href="' . $scripturl . '?action=coppa;member=' . $user_settings['id_member'] . '">' . $txt['coppa_need_more_details'] . '</a>';
522
		return false;
523
	}
524
	// Awaiting approval still?
525
	elseif ($activation_status == 3)
526
		fatal_lang_error('still_awaiting_approval', 'user');
527
	// Awaiting deletion, changed their mind?
528
	elseif ($activation_status == 4)
529
	{
530
		if (isset($_REQUEST['undelete']))
531
		{
532
			updateMemberData($user_settings['id_member'], array('is_activated' => 1));
533
			updateSettings(array('unapprovedMembers' => ($modSettings['unapprovedMembers'] > 0 ? $modSettings['unapprovedMembers'] - 1 : 0)));
534
		}
535
		else
536
		{
537
			$context['disable_login_hashing'] = true;
538
			$context['login_errors'][] = $txt['awaiting_delete_account'];
539
			$context['login_show_undelete'] = true;
540
			return false;
541
		}
542
	}
543
	// Standard activation?
544
	elseif ($activation_status != 1)
545
	{
546
		log_error($txt['activate_not_completed1'] . ' - <span class="remove">' . $user_settings['member_name'] . '</span>', false);
0 ignored issues
show
Documentation introduced by
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...
547
548
		$context['login_errors'][] = $txt['activate_not_completed1'] . ' <a href="' . $scripturl . '?action=activate;sa=resend;u=' . $user_settings['id_member'] . '">' . $txt['activate_not_completed2'] . '</a>';
549
		return false;
550
	}
551
	return true;
552
}
553
554
/**
555
 * Perform the logging in. (set cookie, call hooks, etc)
556
 */
557
function DoLogin()
558
{
559
	global $user_info, $user_settings, $smcFunc;
560
	global $maintenance, $modSettings, $context, $sourcedir;
561
562
	// Load cookie authentication stuff.
563
	require_once($sourcedir . '/Subs-Auth.php');
564
565
	// Call login integration functions.
566
	call_integration_hook('integrate_login', array($user_settings['member_name'], null, $modSettings['cookieTime']));
567
568
	// Get ready to set the cookie...
569
	$user_info['id'] = $user_settings['id_member'];
570
571
	// Bam!  Cookie set.  A session too, just in case.
572
	setLoginCookie(60 * $modSettings['cookieTime'], $user_settings['id_member'], hash_salt($user_settings['passwd'], $user_settings['password_salt']));
573
574
	// Reset the login threshold.
575
	if (isset($_SESSION['failed_login']))
576
		unset($_SESSION['failed_login']);
577
578
	$user_info['is_guest'] = false;
579
	$user_settings['additional_groups'] = explode(',', $user_settings['additional_groups']);
580
	$user_info['is_admin'] = $user_settings['id_group'] == 1 || in_array(1, $user_settings['additional_groups']);
581
582
	// Are you banned?
583
	is_not_banned(true);
584
585
	// Don't stick the language or theme after this point.
586
	unset($_SESSION['language'], $_SESSION['id_theme']);
587
588
	// First login?
589
	$request = $smcFunc['db_query']('', '
590
		SELECT last_login
591
		FROM {db_prefix}members
592
		WHERE id_member = {int:id_member}
593
			AND last_login = 0',
594
		array(
595
			'id_member' => $user_info['id'],
596
		)
597
	);
598
	if ($smcFunc['db_num_rows']($request) == 1)
599
		$_SESSION['first_login'] = true;
600
	else
601
		unset($_SESSION['first_login']);
602
	$smcFunc['db_free_result']($request);
603
604
	// You've logged in, haven't you?
605
	$update = array('member_ip' => $user_info['ip'], 'member_ip2' => $_SERVER['BAN_CHECK_IP']);
606
	if (empty($user_settings['tfa_secret']))
607
		$update['last_login'] = time();
608
	updateMemberData($user_info['id'], $update);
609
610
	// Get rid of the online entry for that old guest....
611
	$smcFunc['db_query']('', '
612
		DELETE FROM {db_prefix}log_online
613
		WHERE session = {string:session}',
614
		array(
615
			'session' => 'ip' . $user_info['ip'],
616
		)
617
	);
618
	$_SESSION['log_time'] = 0;
619
620
	// Log this entry, only if we have it enabled.
621
	if (!empty($modSettings['loginHistoryDays']))
622
		$smcFunc['db_insert']('insert',
623
			'{db_prefix}member_logins',
624
			array(
625
				'id_member' => 'int', 'time' => 'int', 'ip' => 'inet', 'ip2' => 'inet',
626
			),
627
			array(
628
				$user_info['id'], time(), $user_info['ip'], $user_info['ip2']
629
			),
630
			array(
631
				'id_member', 'time'
632
			)
633
		);
634
635
	// Just log you back out if it's in maintenance mode and you AREN'T an admin.
636
	if (empty($maintenance) || allowedTo('admin_forum'))
637
		redirectexit('action=login2;sa=check;member=' . $user_info['id'], $context['server']['needs_login_fix']);
638
	else
639
		redirectexit('action=logout;' . $context['session_var'] . '=' . $context['session_id'], $context['server']['needs_login_fix']);
640
}
641
642
/**
643
 * Logs the current user out of their account.
644
 * It requires that the session hash is sent as well, to prevent automatic logouts by images or javascript.
645
 * It redirects back to $_SESSION['logout_url'], if it exists.
646
 * It is accessed via ?action=logout;session_var=...
647
 *
648
 * @param bool $internal If true, it doesn't check the session
649
 * @param bool $redirect Whether or not to redirect the user after they log out
650
 */
651
function Logout($internal = false, $redirect = true)
652
{
653
	global $sourcedir, $user_info, $user_settings, $context, $smcFunc, $cookiename, $modSettings;
654
655
	// Make sure they aren't being auto-logged out.
656
	if (!$internal)
657
		checkSession('get');
658
659
	require_once($sourcedir . '/Subs-Auth.php');
660
661
	if (isset($_SESSION['pack_ftp']))
662
		$_SESSION['pack_ftp'] = null;
663
664
	// It won't be first login anymore.
665
	unset($_SESSION['first_login']);
666
667
	// Just ensure they aren't a guest!
668
	if (!$user_info['is_guest'])
669
	{
670
		// Pass the logout information to integrations.
671
		call_integration_hook('integrate_logout', array($user_settings['member_name']));
672
673
		// If you log out, you aren't online anymore :P.
674
		$smcFunc['db_query']('', '
675
			DELETE FROM {db_prefix}log_online
676
			WHERE id_member = {int:current_member}',
677
			array(
678
				'current_member' => $user_info['id'],
679
			)
680
		);
681
	}
682
683
	$_SESSION['log_time'] = 0;
684
685
	// Empty the cookie! (set it in the past, and for id_member = 0)
686
	setLoginCookie(-3600, 0);
687
688
	// And some other housekeeping while we're at it.
689
	$salt = substr(md5(mt_rand()), 0, 4);
690
	if (!empty($user_info['id']))
691
		updateMemberData($user_info['id'], array('password_salt' => $salt));
692
693 View Code Duplication
	if (!empty($modSettings['tfa_mode']) && !empty($user_info['id']) && !empty($_COOKIE[$cookiename . '_tfa']))
0 ignored issues
show
Duplication introduced by
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...
694
	{
695
		$tfadata = smf_json_decode($_COOKIE[$cookiename . '_tfa'], true);
696
697
		// If that failed, try the old method
698
		if (is_null($tfadata))
699
			$tfadata = safe_unserialize($_COOKIE[$cookiename . '_tfa']);
700
701
		list ($tfamember, $tfasecret, $exp, $state, $preserve) = $tfadata;
0 ignored issues
show
Unused Code introduced by
The assignment to $tfamember is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $tfasecret is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $state is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
702
703
		// If we're preserving the cookie, reset it with updated salt
704
		if ($preserve && time() < $exp)
705
			setTFACookie(3153600, $user_info['id'], hash_salt($user_settings['tfa_backup'], $salt), true);
706
		else
707
			setTFACookie(-3600, 0, '');
708
	}
709
710
	session_destroy();
711
712
	// Off to the merry board index we go!
713
	if ($redirect)
714
	{
715
		if (empty($_SESSION['logout_url']))
716
			redirectexit('', $context['server']['needs_login_fix']);
717
		elseif (!empty($_SESSION['logout_url']) && (strpos($_SESSION['logout_url'], 'http://') === false && strpos($_SESSION['logout_url'], 'https://') === false))
718
		{
719
			unset ($_SESSION['logout_url']);
720
			redirectexit();
721
		}
722
		else
723
		{
724
			$temp = $_SESSION['logout_url'];
725
			unset($_SESSION['logout_url']);
726
727
			redirectexit($temp, $context['server']['needs_login_fix']);
728
		}
729
	}
730
}
731
732
/**
733
 * MD5 Encryption used for older passwords. (SMF 1.0.x/YaBB SE 1.5.x hashing)
734
 *
735
 * @param string $data The data
736
 * @param string $key The key
737
 * @return string The HMAC MD5 of data with key
738
 */
739
function md5_hmac($data, $key)
740
{
741
	$key = str_pad(strlen($key) <= 64 ? $key : pack('H*', md5($key)), 64, chr(0x00));
742
	return md5(($key ^ str_repeat(chr(0x5c), 64)) . pack('H*', md5(($key ^ str_repeat(chr(0x36), 64)) . $data)));
743
}
744
745
/**
746
 * Custom encryption for phpBB3 based passwords.
747
 *
748
 * @param string $passwd The raw (unhashed) password
749
 * @param string $passwd_hash The hashed password
750
 * @return string The hashed version of $passwd
751
 */
752
function phpBB3_password_check($passwd, $passwd_hash)
753
{
754
	// Too long or too short?
755
	if (strlen($passwd_hash) != 34)
756
		return;
757
758
	// Range of characters allowed.
759
	$range = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
760
761
	// Tests
762
	$strpos = strpos($range, $passwd_hash[3]);
763
	$count = 1 << $strpos;
764
	$salt = substr($passwd_hash, 4, 8);
765
766
	$hash = md5($salt . $passwd, true);
767
	for (; $count != 0; --$count)
768
		$hash = md5($hash . $passwd, true);
769
770
	$output = substr($passwd_hash, 0, 12);
771
	$i = 0;
772
	while ($i < 16)
773
	{
774
		$value = ord($hash[$i++]);
775
		$output .= $range[$value & 0x3f];
776
777
		if ($i < 16)
778
			$value |= ord($hash[$i]) << 8;
779
780
		$output .= $range[($value >> 6) & 0x3f];
781
782
		if ($i++ >= 16)
783
			break;
784
785
		if ($i < 16)
786
			$value |= ord($hash[$i]) << 16;
787
788
		$output .= $range[($value >> 12) & 0x3f];
789
790
		if ($i++ >= 16)
791
			break;
792
793
		$output .= $range[($value >> 18) & 0x3f];
794
	}
795
796
	// Return now.
797
	return $output;
798
}
799
800
/**
801
 * This protects against brute force attacks on a member's password.
802
 * Importantly, even if the password was right we DON'T TELL THEM!
803
 *
804
 * @param int $id_member The ID of the member
805
 * @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 |
806
 * @param bool $was_correct Whether or not the password was correct
807
 * @param bool $tfa Whether we're validating for two-factor authentication
808
 */
809
function validatePasswordFlood($id_member, $password_flood_value = false, $was_correct = false, $tfa = false)
810
{
811
	global $cookiename, $sourcedir;
812
813
	// As this is only brute protection, we allow 5 attempts every 10 seconds.
814
815
	// Destroy any session or cookie data about this member, as they validated wrong.
816
	// Only if they're not validating for 2FA
817
	if (!$tfa)
818
	{
819
		require_once($sourcedir . '/Subs-Auth.php');
820
		setLoginCookie(-3600, 0);
821
822
		if (isset($_SESSION['login_' . $cookiename]))
823
			unset($_SESSION['login_' . $cookiename]);
824
	}
825
826
	// We need a member!
827
	if (!$id_member)
828
	{
829
		// Redirect back!
830
		redirectexit();
831
832
		// Probably not needed, but still make sure...
833
		fatal_lang_error('no_access', false);
834
	}
835
836
	// Right, have we got a flood value?
837
	if ($password_flood_value !== false)
838
		@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...
839
840
	// Timestamp or number of tries invalid?
841
	if (empty($number_tries) || empty($time_stamp))
842
	{
843
		$number_tries = 0;
844
		$time_stamp = time();
845
	}
846
847
	// They've failed logging in already
848
	if (!empty($number_tries))
849
	{
850
		// Give them less chances if they failed before
851
		$number_tries = $time_stamp < time() - 20 ? 2 : $number_tries;
852
853
		// They are trying too fast, make them wait longer
854
		if ($time_stamp < time() - 10)
855
			$time_stamp = time();
856
	}
857
858
	$number_tries++;
859
860
	// Broken the law?
861
	if ($number_tries > 5)
862
		fatal_lang_error('login_threshold_brute_fail', 'critical');
863
864
	// Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it!
865
	updateMemberData($id_member, array('passwd_flood' => $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries));
866
867
}
868
869
?>
0 ignored issues
show
Best Practice introduced by
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...