Issues (1696)

sources/Security.php (49 issues)

1
<?php
2
3
/**
4
 * This file has the very important job of ensuring forum security.
5
 * This task includes banning and permissions, namely.
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 dev
15
 *
16
 */
17
18
use ElkArte\Cache\Cache;
19
use ElkArte\Controller\Auth;
20
use ElkArte\EventManager;
21
use ElkArte\Helper\FileFunctions;
22
use ElkArte\Helper\TokenHash;
23
use ElkArte\Helper\Util;
24
use ElkArte\Http\Headers;
25
use ElkArte\Languages\Txt;
26
use ElkArte\Request;
27
use ElkArte\User;
28
29
/**
30
 * Check if the user is who they say they are.
31
 *
32
 * What it does:
33
 *
34
 * - This function makes sure the user is who they claim to be by requiring a
35
 * password to be typed in every hour.
36
 * - This check can be turned on and off by the securityDisable setting.
37
 * - Uses the adminLogin() function of subs/Auth.subs.php if they need to login,
38
 * which saves all request (POST and GET) data.
39
 *
40
 * @event integrate_validateSession Called at start of validateSession
41
 *
42
 * @param string $type = admin
43
 *
44
 * @return bool|string
45 8
 */
46
function validateSession($type = 'admin')
47
{
48 8
	global $modSettings;
49
50
	// Guests are not welcome here.
51 8
	is_not_guest();
52 8
53 8
	// Validate what type of session check this is.
54
	$types = [];
55
	call_integration_hook('integrate_validateSession', [&$types]);
56 8
	$type = in_array($type, $types, true) || $type === 'moderate' ? $type : 'admin';
57
58 8
	// Set the lifetime for our admin session. Default is ten minutes.
59
	$refreshTime = 10;
60
61 8
	if (isset($modSettings['admin_session_lifetime']))
62
	{
63
		// Maybe someone is paranoid or mistakenly misconfigured the param? Give them at least 5 minutes.
64
		if ($modSettings['admin_session_lifetime'] < 5)
65
		{
66
			$refreshTime = 5;
67 8
		}
68
69
		// A whole day should be more than enough..
70
		elseif ($modSettings['admin_session_lifetime'] > 14400)
71
		{
72
			$refreshTime = 14400;
73
		}
74
75 8
		// We are between our internal min and max. Let's keep the board owner's value.
76
		else
77
		{
78
			$refreshTime = $modSettings['admin_session_lifetime'];
79
		}
80 8
	}
81
82
	// If we're using XML give an additional ten minutes grace as an admin can't log on in XML mode.
83
	if (isset($_GET['api']) && $_GET['api'] === 'xml')
84
	{
85 8
		$refreshTime += 10;
86
	}
87
88
	$refreshTime *= 60;
89 8
90
	// Is the security option off?
91 6
	// @todo remove the exception (means update the db as well)
92
	if (!empty($modSettings['securityDisable' . ($type !== 'admin' ? '_' . $type : '')]))
93
	{
94
		return true;
95 2
	}
96
97 2
	// If their admin or moderator session hasn't expired yet, let it pass, let the admin session trump a moderation one as well
98
	if ((!empty($_SESSION[$type . '_time']) && $_SESSION[$type . '_time'] + $refreshTime >= time()) || (!empty($_SESSION['admin_time']) && $_SESSION['admin_time'] + $refreshTime >= time()))
99
	{
100
		return true;
101
	}
102
103
	require_once(SUBSDIR . '/Auth.subs.php');
104
105
	// Coming from the login screen
106
	if (isset($_POST[$type . '_pass']) || isset($_POST[$type . '_hash_pass']))
107
	{
108
		checkSession();
109
		validateToken('admin-login');
110
111
		// Hashed password, ahoy!
112
		if (isset($_POST[$type . '_hash_pass']) && strlen($_POST[$type . '_hash_pass']) === 64
113
			&& checkPassword($type, true))
114
		{
115
			return true;
116
		}
117
118
		// Posting the password... check it.
119
		if (isset($_POST[$type . '_pass']) && str_replace('*', '', $_POST[$type . '_pass']) !== '' && checkPassword($type))
120
		{
121
			return true;
122
		}
123
	}
124
125
	// Better be sure to remember the real referer
126
	if (empty($_SESSION['request_referer']))
127
	{
128
		$_SESSION['request_referer'] = $_SERVER['HTTP_REFERER'] ?? '';
129
	}
130
	elseif (empty($_POST))
131
	{
132
		unset($_SESSION['request_referer']);
133
	}
134
135
	// Need to type in a password for that, man.
136
	if (!isset($_GET['api']))
137
	{
138
		adminLogin($type);
139
	}
140
141
	return 'session_verify_fail';
142
}
143
144
/**
145
 * Validates a supplied password is correct
146
 *
147
 * What it does:
148
 *
149
 * - Uses integration function to verify password is enabled
150
 * - Uses validateLoginPassword to check using standard ElkArte methods
151
 *
152
 * @event integrate_verify_password allows integration to verify the password
153
 * @param string $type
154
 * @param bool $hash if the supplied password is in _hash_pass
155
 *
156
 * @return bool
157
 */
158
function checkPassword($type, $hash = false)
159
{
160
	$password = $_POST[$type . ($hash ? '_hash_pass' : '_pass')];
161
162
	// Allow integration to verify the password
163
	$good_password = in_array(true, call_integration_hook('integrate_verify_password', [User::$info->username, $password, $hash]), true);
0 ignored issues
show
Bug Best Practice introduced by
The property username does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
164
165
	// Password correct?
166
	if ($good_password || validateLoginPassword($password, User::$info->passwd, $hash ? '' : User::$info->username))
0 ignored issues
show
Bug Best Practice introduced by
The property passwd does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
167
	{
168
		$_SESSION[$type . '_time'] = time();
169
		unset($_SESSION['request_referer']);
170
171
		return true;
172
	}
173
174
	return false;
175
}
176
177
/**
178
 * Require a user who is logged in. (not a guest.)
179
 *
180
 * What it does:
181
 *
182
 * - Checks if the user is currently a guest, and if so asks them to login with a message telling them why.
183
 * - Message is what to tell them when asking them to login.
184
 *
185
 * @param string $message = ''
186
 * @param bool $is_fatal = true
187
 *
188
 * @return bool
189
 */
190
function is_not_guest($message = '', $is_fatal = true)
191
{
192
	global $txt, $context, $scripturl;
193
194
	// Luckily, this person isn't a guest.
195
	if (isset(User::$info->is_guest) && User::$info->is_guest === false)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
196
	{
197
		return true;
198
	}
199
200
	// People always worry when they see people doing things they aren't actually doing...
201
	$_GET['action'] = '';
202
	$_GET['board'] = '';
203
	$_GET['topic'] = '';
204
	writeLog(true);
205
206
	// Just die.
207 14
	if ((isset($_REQUEST['api']) && $_REQUEST['api'] === 'xml') || !$is_fatal)
208
	{
209
		obExit(false);
210 14
	}
211
212 14
	// Attempt to detect if they came from dlattach.
213
	if (ELK !== 'SSI' && empty($context['theme_loaded']))
0 ignored issues
show
The condition ELK !== 'SSI' is always false.
Loading history...
214
	{
215
		new ElkArte\Themes\ThemeLoader();
216
	}
217
218
	// Never redirect to an attachment
219
	if (validLoginUrl($_SERVER['REQUEST_URL']))
220
	{
221
		$_SESSION['login_url'] = $_SERVER['REQUEST_URL'];
222
	}
223
224
	// Load the Login template and language file.
225
	Txt::load('Login');
226
227
	// Apparently we're not in a position to handle this now. Let's go to a safer location for now.
228
	if (!theme()->getLayers()->hasLayers())
229
	{
230
		$_SESSION['login_url'] = $scripturl . '?' . $_SERVER['QUERY_STRING'];
231
		redirectexit('action=login');
232
	}
233
	elseif (isset($_GET['api']))
234
	{
235
		return false;
236
	}
237
	else
238
	{
239
		theme()->getTemplates()->load('Login');
240
		createToken('login');
241
		$context['sub_template'] = 'kick_guest';
242
		$context['robot_no_index'] = true;
243
	}
244
245
	// Use the kick_guest sub template...
246
	$context['kick_message'] = $message;
247
	$context['page_title'] = $txt['login'];
248
	$context['default_password'] = '';
249
250
	obExit();
251
252
	// We should never get to this point, but if we did we wouldn't know the user isn't a guest.
253
	trigger_error('Hacking attempt...', E_USER_ERROR);
254
}
255
256
/**
257
 * Apply restrictions for banned users. For example, disallow access.
258
 *
259
 * What it does:
260
 *
261
 * - If the user is banned, it dies with an error.
262
 * - Caches this information for optimization purposes.
263
 * - Forces a recheck if force_check is true.
264
 *
265
 * @param bool $forceCheck = false
266
 *
267
 * @throws \ElkArte\Exceptions\Exception
268
 */
269
function is_not_banned($forceCheck = false)
270
{
271
	global $txt, $modSettings, $cookiename;
272
273
	$db = database();
274
275
	// You cannot be banned if you are an admin - doesn't help if you log out.
276
	if (User::$info->is_admin)
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
277
	{
278
		return;
279
	}
280
281
	// Only check the ban every so often. (to reduce load.)
282
	if ($forceCheck
283
		|| !isset($_SESSION['ban'])
284
		|| empty($modSettings['banLastUpdated'])
285
		|| ($_SESSION['ban']['last_checked'] < $modSettings['banLastUpdated'])
286
		|| $_SESSION['ban']['id_member'] !== User::$info->id
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
287
		|| $_SESSION['ban']['ip'] !== User::$info->ip
0 ignored issues
show
Bug Best Practice introduced by
The property ip does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
288
		|| $_SESSION['ban']['ip2'] !== User::$info->ip2
0 ignored issues
show
Bug Best Practice introduced by
The property ip2 does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
289
		|| (isset(User::$info->email) && $_SESSION['ban']['email'] !== User::$info->email))
0 ignored issues
show
Bug Best Practice introduced by
The property email does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
290
	{
291
		// Innocent until proven guilty.  (but we know you are! :P)
292
		$_SESSION['ban'] = [
293
			'last_checked' => time(),
294
			'id_member' => User::$info->id,
295
			'ip' => User::$info->ip,
296
			'ip2' => User::$info->ip2,
297
			'email' => User::$info->email,
298
		];
299
300
		$ban_query = [];
301
		$ban_query_vars = ['current_time' => time()];
302
		$flag_is_activated = false;
303
304
		// Check both IP addresses.
305
		foreach (['ip', 'ip2'] as $ip_number)
306
		{
307
			if ($ip_number === 'ip2' && User::$info->ip2 === User::$info->ip)
308
			{
309
				continue;
310
			}
311
312
			$ban_query[] = constructBanQueryIP(User::$info->{$ip_number});
313
314
			// IP was valid, maybe there's also a hostname...
315
			if (empty($modSettings['disableHostnameLookup']) && User::$info->{$ip_number} !== 'unknown')
316
			{
317
				$hostname = host_from_ip(User::$info->{$ip_number});
318
				if ($hostname !== '')
319
				{
320
					$ban_query[] = '({string:hostname} LIKE bi.hostname)';
321
					$ban_query_vars['hostname'] = $hostname;
322
				}
323
			}
324
		}
325
326
		// Is their email address banned?
327
		if (User::$info->email !== '')
328
		{
329
			$ban_query[] = '({string:email} LIKE bi.email_address)';
330
			$ban_query_vars['email'] = User::$info->email;
331
		}
332
333
		// How about this user?
334
		if (User::$info->is_guest === false && !empty(User::$info->id))
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
335
		{
336
			$ban_query[] = 'bi.id_member = {int:id_member}';
337
			$ban_query_vars['id_member'] = User::$info->id;
338
		}
339
340
		// Check the ban, if there's information.
341
		if (!empty($ban_query))
342
		{
343
			$restrictions = [
344
				'cannot_access',
345
				'cannot_login',
346
				'cannot_post',
347
				'cannot_register',
348
			];
349
			$db->fetchQuery('
350
				SELECT 
351
					bi.id_ban, bi.email_address, bi.id_member, bg.cannot_access, bg.cannot_register,
352
					bg.cannot_post, bg.cannot_login, bg.reason, COALESCE(bg.expire_time, 0) AS expire_time
353
				FROM {db_prefix}ban_items AS bi
354
					INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time}))
355
				WHERE
356
					(' . implode(' OR ', $ban_query) . ')',
357
				$ban_query_vars
358
			)->fetch_callback(
359
				static function ($row) use ($restrictions, &$flag_is_activated) {
360
					// Store every type of ban that applies to you in your session.
361
					foreach ($restrictions as $restriction)
362
					{
363
						if (!empty($row[$restriction]))
364
						{
365
							$_SESSION['ban'][$restriction]['reason'] = $row['reason'];
366
							$_SESSION['ban'][$restriction]['ids'][] = $row['id_ban'];
367
							if (!isset($_SESSION['ban']['expire_time']) || ($_SESSION['ban']['expire_time'] != 0 && ($row['expire_time'] == 0 || $row['expire_time'] > $_SESSION['ban']['expire_time'])))
368
							{
369
								$_SESSION['ban']['expire_time'] = $row['expire_time'];
370
							}
371
372
							if (User::$info->is_guest === false && $restriction === 'cannot_access' && ($row['id_member'] == User::$info->id || $row['email_address'] === User::$info->email))
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property email does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
373
							{
374
								$flag_is_activated = true;
375
							}
376
						}
377
					}
378
				}
379
			);
380
		}
381
382
		// Mark the cannot_access and cannot_post bans as being 'hit'.
383
		if (isset($_SESSION['ban']['cannot_access']) || isset($_SESSION['ban']['cannot_post']) || isset($_SESSION['ban']['cannot_login']))
384
		{
385
			log_ban(array_merge(isset($_SESSION['ban']['cannot_access']) ? $_SESSION['ban']['cannot_access']['ids'] : [], isset($_SESSION['ban']['cannot_post']) ? $_SESSION['ban']['cannot_post']['ids'] : [], isset($_SESSION['ban']['cannot_login']) ? $_SESSION['ban']['cannot_login']['ids'] : []));
386
		}
387
388
		// If for whatever reason the is_activated flag seems wrong, do a little work to clear it up.
389
		if (User::$info->id && ((User::$settings['is_activated'] >= 10 && !$flag_is_activated)
390
				|| (User::$settings['is_activated'] < 10 && $flag_is_activated)))
391
		{
392
			require_once(SUBSDIR . '/Bans.subs.php');
393
			updateBanMembers();
394
		}
395
	}
396
397
	// Hey, I know you! You're ehm...
398
	if (!isset($_SESSION['ban']['cannot_access']) && !empty($_COOKIE[$cookiename . '_']))
399
	{
400
		$bans = explode(',', $_COOKIE[$cookiename . '_']);
401
		foreach ($bans as $key => $value)
402
		{
403
			$bans[$key] = (int) $value;
404
		}
405
406
		$db->fetchQuery('
407
			SELECT 
408
				bi.id_ban, bg.reason
409
			FROM {db_prefix}ban_items AS bi
410
				INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
411
			WHERE bi.id_ban IN ({array_int:ban_list})
412
				AND (bg.expire_time IS NULL OR bg.expire_time > {int:current_time})
413
				AND bg.cannot_access = {int:cannot_access}
414
			LIMIT ' . count($bans),
415
			[
416
				'cannot_access' => 1,
417
				'ban_list' => $bans,
418
				'current_time' => time(),
419
			]
420
		)->fetch_callback(
421
			static function ($row) {
422
				$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
423
				$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
424
			}
425
		);
426
427
		// My mistake. Next time better.
428
		if (!isset($_SESSION['ban']['cannot_access']))
429
		{
430
			require_once(SUBSDIR . '/Auth.subs.php');
431
			$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
432
			elk_setcookie($cookiename . '_', '', time() - 3600, $cookie_url[1], $cookie_url[0], false, false);
433
		}
434
	}
435
436
	// If you're fully banned, it's end of the story for you.
437
	if (isset($_SESSION['ban']['cannot_access']))
438
	{
439
		require_once(SUBSDIR . '/Auth.subs.php');
440
441
		// We don't wanna see you!
442
		if (User::$info->is_guest === false)
443
		{
444
			$controller = new Auth(new EventManager());
445
			$controller->setUser(User::$info);
446
			$controller->action_logout(true, false);
447
		}
448
449
		// 'Log' the user out.  Can't have any funny business... (save the name!)
450
		$old_name = (string) User::$info->name !== '' ? User::$info->name : $txt['guest_title'];
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
451
		User::logOutUser(true);
452
		loadUserContext();
453
454
		// A goodbye present.
455
		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
456
		elk_setcookie($cookiename . '_', implode(',', $_SESSION['ban']['cannot_access']['ids']), time() + 3153600, $cookie_url[1], $cookie_url[0], false, false);
457
458
		// Don't scare anyone, now.
459
		$_GET['action'] = '';
460
		$_GET['board'] = '';
461
		$_GET['topic'] = '';
462
		writeLog(true);
463
464
		// You banned, sucka!
465
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $old_name) . (empty($_SESSION['ban']['cannot_access']['reason']) ? '' : '<br />' . $_SESSION['ban']['cannot_access']['reason']) . '<br />' . (empty($_SESSION['ban']['expire_time']) ? $txt['your_ban_expires_never'] : sprintf($txt['your_ban_expires'], standardTime($_SESSION['ban']['expire_time'], false))), 'user');
466
	}
467
468
	// You're not allowed to log in but yet you are. Let's fix that.
469
	if (isset($_SESSION['ban']['cannot_login']) && User::$info->is_guest === false)
470
	{
471
		// We don't wanna see you!
472
		require_once(SUBSDIR . '/Logging.subs.php');
473
		deleteMemberLogOnline();
474
475
		// 'Log' the user out.  Can't have any funny business... (save the name!)
476
		$old_name = (string) User::$info->name !== '' ? User::$info->name : $txt['guest_title'];
477
		User::logOutUser(true);
478
		loadUserContext();
479
480
		// Wipe 'n Clean(r) erases all traces.
481
		$_GET['action'] = '';
482
		$_GET['board'] = '';
483
		$_GET['topic'] = '';
484
		writeLog(true);
485
486
		// Log them out
487
		$controller = new Auth(new EventManager());
488
		$controller->setUser(User::$info);
489
		$controller->action_logout(true, false);
490
491
		// Tell them thanks
492
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $old_name) . (empty($_SESSION['ban']['cannot_login']['reason']) ? '' : '<br />' . $_SESSION['ban']['cannot_login']['reason']) . '<br />' . (empty($_SESSION['ban']['expire_time']) ? $txt['your_ban_expires_never'] : sprintf($txt['your_ban_expires'], standardTime($_SESSION['ban']['expire_time'], false))) . '<br />' . $txt['ban_continue_browse'], 'user');
493
	}
494
495
	// Fix up the banning permissions.
496
	if (!property_exists(User::$info, 'permissions'))
497
	{
498
		return;
499
	}
500
501
	if (User::$info->permissions === null)
0 ignored issues
show
Bug Best Practice introduced by
The property permissions does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
502
	{
503
		return;
504
	}
505
506
	banPermissions();
507
}
508
509
/**
510
 * Fix permissions according to ban status.
511
 *
512
 * What it does:
513
 *
514
 * - Applies any states of banning by removing permissions the user cannot have.
515
 *
516
 * @event integrate_post_ban_permissions Allows to update denied permissions
517
 * @event integrate_warn_permissions Allows changing of permissions for users on warning moderate
518
 * @package Bans
519
 */
520
function banPermissions()
521 1
{
522
	global $modSettings, $context;
523
524 1
	// Somehow they got here, at least take away all permissions...
525
	if (isset($_SESSION['ban']['cannot_access']))
526
	{
527
		User::$info->permissions = [];
0 ignored issues
show
Bug Best Practice introduced by
The property permissions does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
528
	}
529 1
	// Okay, well, you can watch, but don't touch a thing.
530
	elseif (isset($_SESSION['ban']['cannot_post']) || (!empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= User::$info->warning))
0 ignored issues
show
Bug Best Practice introduced by
The property warning does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
531
	{
532
		$denied_permissions = [
533
			'pm_send',
534
			'calendar_post', 'calendar_edit_own', 'calendar_edit_any',
535
			'poll_post',
536
			'poll_add_own', 'poll_add_any',
537
			'poll_edit_own', 'poll_edit_any',
538
			'poll_lock_own', 'poll_lock_any',
539
			'poll_remove_own', 'poll_remove_any',
540
			'manage_attachments', 'manage_smileys', 'manage_boards', 'admin_forum', 'manage_permissions',
541
			'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news',
542
			'profile_identity_any', 'profile_extra_any', 'profile_title_any',
543
			'post_new', 'post_reply_own', 'post_reply_any',
544
			'delete_own', 'delete_any', 'delete_replies',
545
			'make_sticky',
546
			'merge_any', 'split_any',
547
			'modify_own', 'modify_any', 'modify_replies',
548
			'move_any',
549
			'lock_own', 'lock_any',
550
			'remove_own', 'remove_any',
551
			'post_unapproved_topics', 'post_unapproved_replies_own', 'post_unapproved_replies_any',
552
		];
553
		theme()->getLayers()->addAfter('admin_warning', 'body');
554
555
		call_integration_hook('integrate_post_ban_permissions', [&$denied_permissions]);
556
		User::$info->permissions = array_diff(User::$info->permissions, $denied_permissions);
0 ignored issues
show
It seems like ElkArte\User::info->permissions can also be of type null; however, parameter $array of array_diff() 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

556
		User::$info->permissions = array_diff(/** @scrutinizer ignore-type */ User::$info->permissions, $denied_permissions);
Loading history...
Bug Best Practice introduced by
The property permissions does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
557
	}
558
	// Are they absolutely under moderation?
559 1
	elseif (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= User::$info->warning)
560
	{
561
		// Work out what permissions should change...
562
		$permission_change = [
563
			'post_new' => 'post_unapproved_topics',
564
			'post_reply_own' => 'post_unapproved_replies_own',
565
			'post_reply_any' => 'post_unapproved_replies_any',
566
			'post_attachment' => 'post_unapproved_attachments',
567
		];
568
		call_integration_hook('integrate_warn_permissions', [&$permission_change]);
569
		foreach ($permission_change as $old => $new)
570
		{
571
			if (!in_array($old, User::$info->permissions))
0 ignored issues
show
It seems like ElkArte\User::info->permissions can also be of type null; however, parameter $haystack of in_array() 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

571
			if (!in_array($old, /** @scrutinizer ignore-type */ User::$info->permissions))
Loading history...
572
			{
573
				unset($permission_change[$old]);
574
			}
575
			else
576
			{
577
				User::$info->permissions = array_merge((array) User::$info->permissions, $new);
0 ignored issues
show
$new of type string is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

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

577
				User::$info->permissions = array_merge((array) User::$info->permissions, /** @scrutinizer ignore-type */ $new);
Loading history...
578
			}
579
		}
580
581
		User::$info->permissions = array_diff(User::$info->permissions, array_keys($permission_change));
582
	}
583
584
	// @todo Find a better place to call this? Needs to be after permissions loaded!
585 1
	// Finally, some bits we cache in the session because it saves queries.
586
	if (isset($_SESSION['mc']) && $_SESSION['mc']['time'] > $modSettings['settings_updated'] && $_SESSION['mc']['id'] == User::$info->id)
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
587
	{
588
		User::$info->mod_cache = $_SESSION['mc'];
0 ignored issues
show
Bug Best Practice introduced by
The property mod_cache does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __set, consider adding a @property annotation.
Loading history...
589
	}
590
	else
591 1
	{
592 1
		require_once(SUBSDIR . '/Auth.subs.php');
593
		rebuildModCache();
594
	}
595
596 1
	// Now that we have the mod cache taken care of lets setup a cache for the number of mod reports still open
597
	if (isset($_SESSION['rc']) && $_SESSION['rc']['time'] > $modSettings['last_mod_report_action'] && $_SESSION['rc']['id'] == User::$info->id)
598
	{
599
		$context['open_mod_reports'] = $_SESSION['rc']['reports'];
600
		if (allowedTo('admin_forum'))
601
		{
602
			$context['open_pm_reports'] = $_SESSION['rc']['pm_reports'];
603
		}
604 1
	}
605
	elseif ($_SESSION['mc']['bq'] != '0=1')
606
	{
607
		require_once(SUBSDIR . '/Moderation.subs.php');
608
		recountOpenReports(true, allowedTo('admin_forum'));
609
	}
610
	else
611 1
	{
612
		$context['open_mod_reports'] = 0;
613 1
	}
614
}
615
616
/**
617
 * Log a ban in the database.
618
 *
619
 * What it does:
620
 *
621
 * - Log the current user in the ban logs.
622
 * - Increment the hit counters for the specified ban ID's (if any.)
623
 *
624
 * @param int[] $ban_ids = array()
625
 * @param string|null $email = null
626
 * @package Bans
627
 */
628
function log_ban($ban_ids = [], $email = null)
629
{
630
	$db = database();
631
632
	// Don't log web accelerators, it's very confusing...
633
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] === 'prefetch')
634
	{
635
		return;
636
	}
637
638
	$db->insert('',
639
		'{db_prefix}log_banned',
640
		[
641
			'id_member' => 'int',
642
			'ip' => 'string-16',
643
			'email' => 'string',
644
			'log_time' => 'int'
645
		],
646
		[
647
			User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
648
			User::$info->ip,
0 ignored issues
show
Bug Best Practice introduced by
The property ip does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
649
			$email ?? (string) User::$info->email,
0 ignored issues
show
Bug Best Practice introduced by
The property email does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
650
			time()
651
		],
652
		['id_ban_log']
653
	);
654
655
	// One extra point for these bans.
656
	if (!empty($ban_ids))
657
	{
658
		$db->query('', '
659
			UPDATE {db_prefix}ban_items
660
			SET hits = hits + 1
661
			WHERE id_ban IN ({array_int:ban_ids})',
662
			[
663
				'ban_ids' => $ban_ids,
664
			]
665
		);
666
	}
667
}
668
669
/**
670
 * Checks if a given email address might be banned.
671
 *
672
 * What it does:
673
 *
674
 * - Check if a given email is banned.
675
 * - Performs an immediate ban if the turns turns out positive.
676
 *
677
 * @param string $email
678
 * @param string $restriction
679
 * @param string $error
680
 *
681
 * @throws \ElkArte\Exceptions\Exception
682
 * @package Bans
683
 */
684
function isBannedEmail($email, $restriction, $error)
685
{
686 2
	global $txt;
687
688 2
	$db = database();
689
690
	// Can't ban an empty email
691 2
	if (empty($email) || trim($email) === '')
692
	{
693
		return;
694
	}
695
696
	// Let's start with the bans based on your IP/hostname/memberID...
697 2
	$ban_ids = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['ids'] : [];
698 2
	$ban_reason = isset($_SESSION['ban'][$restriction]) ? $_SESSION['ban'][$restriction]['reason'] : '';
699
700
	// ...and add to that the email address you're trying to register.
701 2
	$db->fetchQuery('
702
		SELECT 
703 2
			bi.id_ban, bg.' . $restriction . ', bg.cannot_access, bg.reason
704
		FROM {db_prefix}ban_items AS bi
705
			INNER JOIN {db_prefix}ban_groups AS bg ON (bg.id_ban_group = bi.id_ban_group)
706
		WHERE {string:email} LIKE bi.email_address
707 2
			AND (bg.' . $restriction . ' = {int:cannot_access} OR bg.cannot_access = {int:cannot_access})
708
			AND (bg.expire_time IS NULL OR bg.expire_time >= {int:now})',
709
		[
710 2
			'email' => $email,
711 2
			'cannot_access' => 1,
712 2
			'now' => time(),
713
		]
714 2
	)->fetch_callback(
715
		static function ($row) use (&$ban_ids, &$ban_reason, $restriction) {
716
			if (!empty($row['cannot_access']))
717
			{
718
				$_SESSION['ban']['cannot_access']['ids'][] = $row['id_ban'];
719
				$_SESSION['ban']['cannot_access']['reason'] = $row['reason'];
720
			}
721
722
			if (!empty($row[$restriction]))
723
			{
724
				$ban_ids[] = $row['id_ban'];
725
				$ban_reason = $row['reason'];
726
			}
727 2
		}
728
	);
729
730
	// You're in biiig trouble.  Banned for the rest of this session!
731 2
	if (isset($_SESSION['ban']['cannot_access']))
732
	{
733
		log_ban($_SESSION['ban']['cannot_access']['ids']);
734
		$_SESSION['ban']['last_checked'] = time();
735
736
		throw new \ElkArte\Exceptions\Exception(sprintf($txt['your_ban'], $txt['guest_title']) . $_SESSION['ban']['cannot_access']['reason'], false);
737
	}
738
739 2
	if (!empty($ban_ids))
740
	{
741
		// Log this ban for future reference.
742
		log_ban($ban_ids, $email);
743
		throw new \ElkArte\Exceptions\Exception($error . $ban_reason, false);
744
	}
745 2
}
746
747
/**
748
 * Make sure the user's correct session was passed, and they came from here.
749
 *
750
 * What it does:
751
 *
752
 * - Checks the current session, verifying that the person is who he or she should be.
753
 * - Also checks the referrer to make sure they didn't get sent here.
754
 * - Depends on the disableCheckUA setting, which is usually missing.
755
 * - Will check GET, POST, or REQUEST depending on the passed type.
756
 * - Also optionally checks the referring action if passed. (note that the referring action must be by GET.)
757
 *
758
 * @param string $type = 'post' (post, get, request)
759
 * @param string $from_action = ''
760
 * @param bool $is_fatal = true
761
 *
762
 * @return string the error message if is_fatal is false.
763
 */
764
function checkSession($type = 'post', $from_action = '', $is_fatal = true)
765
{
766
	global $modSettings, $boardurl;
767 18
768
	// We'll work out user agent checks
769
	$req = Request::instance();
770 18
771
	// Is it in as $_POST['sc']?
772
	if ($type === 'post')
773 18
	{
774
		$check = $_POST[$_SESSION['session_var']] ?? (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']) ? $_POST['sc'] : null);
775 14
		if ($check !== $_SESSION['session_value'])
776 14
		{
777
			$error = 'session_timeout';
778 14
		}
779
	}
780
	// How about $_GET['sesc']?
781
	elseif ($type === 'get')
782 4
	{
783
		$check = $_GET[$_SESSION['session_var']] ?? (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']) ? $_GET['sesc'] : null);
784 2
		if ($check !== $_SESSION['session_value'])
785 2
		{
786
			$error = 'session_verify_fail';
787 2
		}
788
	}
789
	// Or can it be in either?
790
	elseif ($type === 'request')
791 2
	{
792
		$check = null;
793 2
		if (isset($_GET[$_SESSION['session_var']]))
794
		{
795 2
			$check = $_GET[$_SESSION['session_var']];
796
		}
797
		elseif (empty($modSettings['strictSessionCheck']) && isset($_GET['sesc']))
798
		{
799
			$check = $_GET['sesc'];
800
		}
801
		elseif (isset($_POST[$_SESSION['session_var']]))
802 18
		{
803
			$check = $_POST[$_SESSION['session_var']];
804
		}
805
		elseif (empty($modSettings['strictSessionCheck']) && isset($_POST['sc']))
806
		{
807
			$check = $_POST['sc'];
808 18
		}
809
810
		if ($check !== $_SESSION['session_value'])
811 18
		{
812
			$error = 'session_verify_fail';
813
		}
814
	}
815
816
	// Verify that they aren't changing user agents on us - that could be bad.
817 18
	if ((!isset($_SESSION['USER_AGENT']) || $_SESSION['USER_AGENT'] !== $req->user_agent()) && empty($modSettings['disableCheckUA']))
818
	{
819
		$error = 'session_verify_fail';
820 18
	}
821
822 18
	// Make sure a page with session check requirement is not being prefetched.
823
	stop_prefetching();
824
825
	// Check the referring site - it should be the same server at least!
826
827
	$referrer_url = $_SESSION['request_referer'] ?? ($_SERVER['HTTP_REFERER'] ?? '');
828
829
	$referrer = @parse_url($referrer_url);
830
	if (!empty($referrer['host']))
831
	{
832
		if (str_contains($_SERVER['HTTP_HOST'], ':'))
833
		{
834
			$real_host = substr($_SERVER['HTTP_HOST'], 0, strpos($_SERVER['HTTP_HOST'], ':'));
835
		}
836
		else
837
		{
838
			$real_host = $_SERVER['HTTP_HOST'];
839
		}
840
841
		$parsed_url = parse_url($boardurl);
842
843
		// Are global cookies on? If so, let's check them ;).
844
		if (!empty($modSettings['globalCookies']))
845
		{
846
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
847
			{
848
				$parsed_url['host'] = $parts[1];
849
			}
850
851
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $referrer['host'], $parts) == 1)
852
			{
853
				$referrer['host'] = $parts[1];
854
			}
855
856
			if (preg_match('~(?:[^\.]+\.)?([^\.]{3,}\..+)\z~i', $real_host, $parts) == 1)
857
			{
858
				$real_host = $parts[1];
859
			}
860
		}
861
862
		// Okay: referrer must either match parsed_url or real_host.
863
		if (isset($parsed_url['host']) && strtolower($referrer['host']) !== strtolower($parsed_url['host']) && strtolower($referrer['host']) !== strtolower($real_host))
864 18
		{
865
			$error = 'verify_url_fail';
866
			$log_error = true;
867
			$sprintf = [Util::htmlspecialchars($referrer_url)];
868
		}
869
	}
870
871
	// Well, first of all, if a from_action is specified you'd better have an old_url.
872 18
	if (!empty($from_action) && (!isset($_SESSION['old_url']) || preg_match('~[?;&]action=' . $from_action . '([;&]|$)~', $_SESSION['old_url']) !== 1))
873
	{
874 18
		$error = 'verify_url_fail';
875
		$log_error = true;
876
		$sprintf = [Util::htmlspecialchars($referrer_url)];
877
	}
878
879
	// Everything is ok, return an empty string.
880
	if (!isset($error))
881
	{
882
		return '';
883
	}
884
885
	// A session error occurred, show the error.
886
	if ($is_fatal)
887
	{
888
		if (isset($_REQUEST['api']))
889
		{
890
			@ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ob_end_clean(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

890
			/** @scrutinizer ignore-unhandled */ @ob_end_clean();

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...
891
			Headers::instance()
892
				->removeHeader('all')
893
				->httpCode(403)
894
				->header('X-Error-Message', 'Session timeout')
895
				->sendHeaders();
896
			die;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
897
		}
898
899
		throw new \ElkArte\Exceptions\Exception($error, isset($log_error) ? 'user' : false, $sprintf ?? []);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $error does not seem to be defined for all execution paths leading up to this point.
Loading history...
900
	}
901
902
	// A session error occurred, return the error to the calling function.
903
	return $error;
904
905
	// We really should never fall through here, for very important reasons.  Let's make sure.
906
	trigger_error('Hacking attempt...', E_USER_ERROR);
0 ignored issues
show
trigger_error('Hacking attempt...', E_USER_ERROR) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
907
}
908
909
/**
910
 * Let's give you a token of our appreciation.
911
 *
912
 * What it does:
913
 *
914 13
 * - Creates a one time use form token
915
 *
916
 * @param string $action The specific site action that a token will be generated for
917 13
 * @param string $type = 'post' If the token will be returned via post or get
918 13
 *
919 13
 * @return string[] array of token var, time, csrf, token
920
 */
921
function createToken($action, $type = 'post')
922 13
{
923 13
	global $context;
924
925
	// Generate a new token token_var pair
926 13
	$tokenizer = new TokenHash();
927 13
	$token_var = $tokenizer->generate_hash(rand(7, 12));
928 13
	$token = $tokenizer->generate_hash(32);
929
930 13
	// We need user agent and the client IP
931
	$req = Request::instance();
932
	$csrf_hash = hash('sha1', $token . $req->client_ip() . $req->user_agent());
933
934
	// Save the session token and make it available to the forms
935
	$_SESSION['token'][$type . '-' . $action] = [$token_var, $csrf_hash, time(), $token];
936
	$context[$action . '_token'] = $token;
937
	$context[$action . '_token_var'] = $token_var;
938
939
	return [$action . '_token_var' => $token_var, $action . '_token' => $token];
940
}
941
942
/**
943
 * Only patrons with valid tokens can ride this ride.
944
 *
945
 * What it does:
946
 *
947
 * Validates that the received token is correct
948
 *  1. The token exists in session.
949
 *  2. The {$type} variable should exist.
950
 *  3. We concatenate the variable we received with the user agent
951
 *  4. Match that result against what is in the session.
952
 *  5. If it matches, success, otherwise we fallout.
953
 *
954
 * @param string $action
955 8
 * @param string $type = 'post' (get, request, or post)
956 8
 * @param bool $reset = true Reset the token on failure
957
 * @param bool $fatal if true a fatal_lang_error is issued for invalid tokens, otherwise false is returned
958
 *
959 8
 * @return bool|string except for $action == 'login' where the token is returned
960
 * @throws \ElkArte\Exceptions\Exception token_verify_fail
961 2
 */
962
function validateToken($action, $type = 'post', $reset = true, $fatal = true)
963 2
{
964 2
	$type = ($type === 'get' || $type === 'request') ? $type : 'post';
965
	$token_index = $type . '-' . $action;
966 2
967
	// Logins are special: the token is used to have the password with javascript before POST it
968
	if ($action === 'login')
969
	{
970
		if (isset($_SESSION['token'][$token_index]))
971
		{
972 6
			$return = $_SESSION['token'][$token_index][3];
973
			unset($_SESSION['token'][$token_index]);
974 6
975
			return $return;
976
		}
977
978
		return '';
979
	}
980
981
	if (!isset($_SESSION['token'][$token_index]))
982
	{
983
		return false;
984
	}
985
986
	// We need the user agent and client IP
987
	$req = Request::instance();
988
989
	// Shortcut
990
	$passed_token_var = $GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$token_index][0]] ?? null;
991
	$csrf_hash = hash('sha1', $passed_token_var . $req->client_ip() . $req->user_agent());
992
993
	// Checked what was passed in combination with the user agent
994
	if (isset($passed_token_var)
995
		&& $csrf_hash === $_SESSION['token'][$token_index][1])
996
	{
997
		// Consume the token, let them pass
998
		unset($_SESSION['token'][$token_index]);
999
1000
		return true;
1001
	}
1002
1003
	// Patrons with invalid tokens get the boot.
1004
	if ($reset)
1005
	{
1006
		// Might as well do some cleanup on this.
1007
		cleanTokens();
1008
1009
		// I'm back baby.
1010
		createToken($action, $type);
1011
1012
		if ($fatal)
1013
		{
1014
			throw new \ElkArte\Exceptions\Exception('token_verify_fail', false);
1015
		}
1016
	}
1017
	// You don't get a new token
1018
	else
1019
	{
1020
		// Explicitly remove this token
1021
		unset($_SESSION['token'][$token_index]);
1022
1023
		// Remove older tokens.
1024
		cleanTokens();
1025
	}
1026
1027
	return false;
1028
}
1029
1030
/**
1031
 * Removes old unused tokens from session
1032
 *
1033
 * What it does:
1034
 *
1035 1
 * - Defaults to 3 hours before a token is considered expired
1036
 * - if $complete = true will remove all tokens
1037
 *
1038
 * @param bool $complete = false
1039
 * @param string $suffix = false
1040
 */
1041 1
function cleanTokens($complete = false, $suffix = '')
1042
{
1043 1
	// We appreciate cleaning up after yourselves.
1044
	if (!isset($_SESSION['token']))
1045
	{
1046
		return;
1047
	}
1048
1049 1
	// Clean up tokens, trying to give enough time still.
1050
	foreach ($_SESSION['token'] as $key => $data)
1051
	{
1052 1
		$force = empty($suffix) ? $complete : $complete || strpos($key, $suffix);
1053
1054
		if ($data[2] + 10800 < time() || $force)
1055
		{
1056
			unset($_SESSION['token'][$key]);
1057 1
		}
1058
	}
1059
}
1060
1061
/**
1062
 * Check whether a form has been submitted twice.
1063
 *
1064
 * What it does:
1065
 *
1066
 * - Registers a sequence number for a form.
1067
 * - Checks whether a submitted sequence number is registered in the current session.
1068
 * - Depending on the value of is_fatal shows an error or returns true or false.
1069
 * - Frees a sequence number from the stack after it's been checked.
1070
 * - Frees a sequence number without checking if action == 'free'.
1071
 *
1072
 * @param string $action
1073
 * @param bool $is_fatal = true
1074
 *
1075
 * @return bool|void
1076
 * @throws \ElkArte\Exceptions\Exception error_form_already_submitted
1077
 */
1078 12
function checkSubmitOnce($action, $is_fatal = false)
1079
{
1080 12
	global $context;
1081
1082 12
	if (!isset($_SESSION['forms']))
1083
	{
1084
		$_SESSION['forms'] = [];
1085
	}
1086 12
1087
	// Register a form number and store it in the session stack. (use this on the page that has the form.)
1088 6
	if ($action === 'register')
1089 6
	{
1090 6
		$tokenizer = new TokenHash();
1091
		$context['form_sequence_number'] = '';
1092 6
		while (empty($context['form_sequence_number']) || in_array($context['form_sequence_number'], $_SESSION['forms'], true))
1093
		{
1094
			$context['form_sequence_number'] = $tokenizer->generate_hash();
1095
		}
1096 8
	}
1097
	// Check whether the submitted number can be found in the session.
1098 8
	elseif ($action === 'check')
1099
	{
1100 8
		if (!isset($_REQUEST['seqnum']))
1101
		{
1102
			return true;
1103
		}
1104
1105
		if (!in_array($_REQUEST['seqnum'], $_SESSION['forms'], true))
1106
		{
1107
			// Mark this one as used
1108
			$_SESSION['forms'][] = (string) $_REQUEST['seqnum'];
1109
			return true;
1110
		}
1111
1112
		if ($is_fatal)
1113
		{
1114
			throw new \ElkArte\Exceptions\Exception('error_form_already_submitted', false);
1115
		}
1116
1117
		return false;
1118
	}
1119
	// Don't check, just free the stack number.
1120
	elseif ($action === 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms'], true))
1121
	{
1122
		$_SESSION['forms'] = array_diff($_SESSION['forms'], [$_REQUEST['seqnum']]);
1123
	}
1124
	elseif ($action !== 'free')
1125
	{
1126
		trigger_error("checkSubmitOnce(): Invalid action '" . $action . "'", E_USER_WARNING);
1127 6
	}
1128
}
1129
1130
/**
1131
 * This function checks whether the user is allowed to do permission. (ie. post_new.)
1132
 *
1133
 * What it does:
1134
 *
1135
 * - If boards parameter is specified, checks those boards instead of the current one (if applicable).
1136
 * - Always returns true if the user is an administrator.
1137
 *
1138
 * @param string[]|string $permission permission
1139
 * @param int[]|int|null $boards array of board IDs, a single id or null
1140
 *
1141
 * @return bool if the user can do the permission
1142
 */
1143
function allowedTo($permission, $boards = null)
1144
{
1145 330
	$db = database();
1146
1147
	// You're always allowed to do nothing. (unless you're a working man, MR. LAZY :P!)
1148 330
	if (empty($permission))
1149
	{
1150 12
		return true;
1151
	}
1152
1153
	// You're never allowed to do something if your data hasn't been loaded yet!
1154 330
	if (empty(User::$info) || !isset(User::$info['permissions']))
1155
	{
1156
		return false;
1157
	}
1158
1159
	// Administrators are supermen :P.
1160 330
	if (User::$info->is_admin)
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1161
	{
1162 306
		return true;
1163
	}
1164
1165
	// Make sure permission is a valid array
1166 24
	if (!is_array($permission))
1167
	{
1168 22
		$permission = [$permission];
1169
	}
1170
1171
	// Are we checking the _current_ board, or some other boards?
1172 24
	if ($boards === null)
1173
	{
1174 24
		if (empty(User::$info->permissions))
0 ignored issues
show
Bug Best Practice introduced by
The property permissions does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1175
		{
1176 12
			return false;
1177
		}
1178
1179
		// Check if they can do it, you aren't allowed, by default.
1180 12
		return array_intersect($permission, User::$info->permissions) !== [];
1181
	}
1182
1183
	if (!is_array($boards))
1184
	{
1185
		$boards = [$boards];
1186
	}
1187
1188
	if (empty(User::$info->groups))
0 ignored issues
show
Bug Best Practice introduced by
The property groups does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1189
	{
1190
		return false;
1191
	}
1192
1193
	$request = $db->query('', '
1194
		SELECT 
1195
			MIN(bp.add_deny) AS add_deny
1196
		FROM {db_prefix}boards AS b
1197
			INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile)
1198
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1199
		WHERE b.id_board IN ({array_int:board_list})
1200
			AND bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1201
			AND bp.permission IN ({array_string:permission_list})
1202
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})
1203
		GROUP BY b.id_board',
1204
		[
1205
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1206
			'board_list' => $boards,
1207
			'group_list' => User::$info->groups,
1208
			'moderator_group' => 3,
1209
			'permission_list' => $permission,
1210
		]
1211
	);
1212
1213
	// Make sure they can do it on all the boards.
1214
	if ($request->num_rows() !== count($boards))
1215
	{
1216
		return false;
1217
	}
1218
1219
	$result = true;
1220
	while (($row = $request->fetch_assoc()))
1221
	{
1222
		$result = $result && !empty($row['add_deny']);
1223
	}
1224
1225
	$request->free_result();
1226
1227
	// If the query returned 1, they can do it... otherwise, they can't.
1228
	return $result;
1229
}
1230
1231
/**
1232
 * This function returns fatal error if the user doesn't have the respective permission.
1233
 *
1234
 * What it does:
1235
 *
1236
 * - Uses allowedTo() to check if the user is allowed to do permission.
1237
 * - Checks the passed boards or current board for the permission.
1238
 * - If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission].
1239
 * - If they are a guest and cannot do it, this calls is_not_guest().
1240
 *
1241
 * @param string[]|string $permission array of or single string, of permissions to check
1242
 * @param int[]|null $boards = null
1243
 *
1244
 * @throws \ElkArte\Exceptions\Exception cannot_xyz where xyz is the permission
1245
 */
1246
function isAllowedTo($permission, $boards = null)
1247 68
{
1248
	global $txt;
1249 68
1250
	static $heavy_permissions = [
1251
		'admin_forum',
1252
		'manage_attachments',
1253
		'manage_smileys',
1254
		'manage_boards',
1255
		'edit_news',
1256
		'moderate_forum',
1257
		'manage_bans',
1258
		'manage_membergroups',
1259
		'manage_permissions',
1260
	];
1261
1262 68
	// Make it an array, even if a string was passed.
1263
	$permission = is_array($permission) ? $permission : [$permission];
1264
1265 68
	// Check the permission and return an error...
1266
	if (!allowedTo($permission, $boards))
1267
	{
1268
		// Pick the last array entry as the permission shown as the error.
1269
		$error_permission = array_shift($permission);
1270
1271
		// If they are a guest, show a login. (because the error might be gone if they do!)
1272
		if (User::$info->is_guest)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1273
		{
1274
			Txt::load('Errors');
1275
			is_not_guest($txt['cannot_' . $error_permission]);
1276
		}
1277
1278
		// Clear the action because they aren't really doing that!
1279
		$_GET['action'] = '';
1280
		$_GET['board'] = '';
1281
		$_GET['topic'] = '';
1282
		writeLog(true);
1283
1284
		throw new \ElkArte\Exceptions\Exception('cannot_' . $error_permission, false);
1285
	}
1286
1287
	// If you're doing something on behalf of some "heavy" permissions, validate your session.
1288 68
	// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.)
1289
	if (!allowedTo(array_diff($permission, $heavy_permissions), $boards))
1290
	{
1291
		validateSession();
1292 68
	}
1293
}
1294
1295
/**
1296
 * Return the boards a user has a certain (board) permission on. (array(0) if all.)
1297
 *
1298
 * What it does:
1299
 *
1300
 * - Returns a list of boards on which the user is allowed to do the specified permission.
1301
 * - Returns an array with only a 0 in it if the user has permission to do this on every board.
1302
 * - Returns an empty array if he or she cannot do this on any board.
1303
 * - If check_access is true will also make sure the group has proper access to that board.
1304
 *
1305
 * @param string[]|string $permissions array of permission names to check access against
1306
 * @param bool $check_access = true
1307
 * @param bool $simple = true Set $simple to true to use this function in compatibility mode
1308
 *             otherwise, the resultant array becomes split into the multiple
1309
 *             permissions that were passed. Other than that, it's just the normal
1310
 *             state of play that you're used to.
1311
 *
1312
 * @return int[]
1313
 * @throws \ElkArte\Exceptions\Exception
1314
 */
1315
function boardsAllowedTo($permissions, $check_access = true, $simple = true)
1316 3
{
1317
	$db = database();
1318
1319 3
	// Arrays are nice, most of the time.
1320
	if (!is_array($permissions))
1321 3
	{
1322
		$permissions = [$permissions];
1323
	}
1324
1325 3
	// I am the master, the master of the universe!
1326
	if (User::$info->is_admin)
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1327 2
	{
1328
		if ($simple)
1329 2
		{
1330
			return [0];
1331
		}
1332
1333
		$boards = [];
1334
		foreach ($permissions as $permission)
1335
		{
1336
			$boards[$permission] = [0];
1337
		}
1338
1339
		return $boards;
1340
	}
1341
1342
	// All groups the user is in except 'moderator'.
1343
	$groups = array_diff(User::$info->groups, [3]);
0 ignored issues
show
It seems like ElkArte\User::info->groups can also be of type null; however, parameter $array of array_diff() 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

1343
	$groups = array_diff(/** @scrutinizer ignore-type */ User::$info->groups, [3]);
Loading history...
Bug Best Practice introduced by
The property groups does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1344 1
1345
	$boards = [];
1346 1
	$deny_boards = [];
1347 1
	$db->fetchQuery('
1348 1
		SELECT 
1349
			b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . '
1350 1
		FROM {db_prefix}board_permissions AS bp
1351
			INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile)
1352
			LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member})
1353
		WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group})
1354
			AND bp.permission IN ({array_string:permissions})
1355
			AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})' .
1356
		($check_access ? ' AND {query_see_board}' : ''),
1357 1
		[
1358
			'current_member' => User::$info->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1359 1
			'group_list' => $groups,
1360 1
			'moderator_group' => 3,
1361 1
			'permissions' => $permissions,
1362 1
		]
1363
	)->fetch_callback(
1364 1
		static function ($row) use ($simple, &$deny_boards, &$boards) {
1365
			if ($simple)
1366
			{
1367
				if (empty($row['add_deny']))
1368
				{
1369
					$deny_boards[] = (int) $row['id_board'];
1370
				}
1371
				else
1372
				{
1373
					$boards[] = (int) $row['id_board'];
1374
				}
1375
			}
1376
			elseif (empty($row['add_deny']))
1377
			{
1378
				$deny_boards[$row['permission']][] = (int) $row['id_board'];
1379
			}
1380
			else
1381
			{
1382
				$boards[$row['permission']][] = (int) $row['id_board'];
1383
			}
1384
		}
1385 1
	);
1386
1387
	if ($simple)
1388 1
	{
1389
		$boards = array_unique(array_values(array_diff($boards, $deny_boards)));
1390 1
	}
1391
	else
1392
	{
1393
		foreach ($permissions as $permission)
1394
		{
1395
			// Never had it to start with
1396
			if (empty($boards[$permission]))
1397
			{
1398
				$boards[$permission] = [];
1399
			}
1400
			else
1401
			{
1402
				// Or it may have been removed
1403
				$deny_boards[$permission] = $deny_boards[$permission] ?? [];
1404
				$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission])));
1405
			}
1406
		}
1407
	}
1408
1409
	return $boards;
1410 1
}
1411
1412
/**
1413
 * Returns whether an email address should be shown and how.
1414
 *
1415
 * What it does:
1416
 *
1417
 * Possible outcomes are:
1418
 *  If it's your own profile yes.
1419
 *  If you're a moderator with sufficient permissions: yes.
1420
 *  Otherwise: no
1421
 *
1422
 * @param int $userProfile_id
1423
 *
1424
 * @return bool
1425
 */
1426
function showEmailAddress($userProfile_id)
1427
{
1428
	// Should this user's email address be shown?
1429
	if ((User::$info->is_guest === false && User::$info->id === (int) $userProfile_id))
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1430
	{
1431
		return true;
1432
	}
1433
1434
	if (allowedTo('moderate_forum'))
1435
	{
1436
		return true;
1437
	}
1438
1439
	return false;
1440
}
1441 8
1442
/**
1443
 * This function attempts to protect from carrying out specific actions repeatedly.
1444
 *
1445
 * What it does:
1446 8
 *
1447
 * - Checks if a user is trying specific actions faster than a given minimum wait threshold.
1448 6
 * - The time taken depends on error_type - generally uses the modSetting.
1449
 * - Generates a fatal message when triggered, suspending execution.
1450
 *
1451 2
 * @event integrate_spam_protection Allows updating action wait timeOverrides
1452
 * @param string $error_type used also as a $txt index. (not an actual string.)
1453 2
 * @param bool $fatal is the spam check a fatal error on failure
1454
 *
1455
 * @return bool|int|mixed
1456
 * @throws \ElkArte\Exceptions\Exception
1457
 */
1458
function spamProtection($error_type, $fatal = true)
1459
{
1460
	global $modSettings;
1461
1462
	$db = database();
1463
1464
	// Certain types take less/more time.
1465
	$timeOverrides = [
1466
		'login' => 2,
1467
		'register' => 2,
1468
		'remind' => 30,
1469
		'contact' => 30,
1470
		'sendmail' => $modSettings['spamWaitTime'] * 5,
1471
		'reporttm' => $modSettings['spamWaitTime'] * 4,
1472
		'search' => empty($modSettings['search_floodcontrol_time']) ? 1 : $modSettings['search_floodcontrol_time'],
1473
	];
1474
	call_integration_hook('integrate_spam_protection', [&$timeOverrides]);
1475
1476
	// Moderators are free...
1477
	$timeLimit = allowedTo('moderate_board') ? 2 : $timeOverrides[$error_type] ?? $modSettings['spamWaitTime'];
1478
1479
	// Delete old entries...
1480
	$db->query('', '
1481
		DELETE FROM {db_prefix}log_floodcontrol
1482 12
		WHERE log_time < {int:log_time}
1483
			AND log_type = {string:log_type}',
1484 12
		[
1485
			'log_time' => time() - $timeLimit,
1486
			'log_type' => $error_type,
1487
		]
1488 12
	);
1489 12
1490 12
	// Add a new entry, deleting the old if necessary.
1491 12
	$request = $db->replace(
1492 12
		'{db_prefix}log_floodcontrol',
1493 12
		['ip' => 'string-16', 'log_time' => 'int', 'log_type' => 'string'],
1494 12
		[User::$info->ip, time(), $error_type],
0 ignored issues
show
Bug Best Practice introduced by
The property ip does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1495 12
		['ip', 'log_type']
1496
	);
1497 12
1498
	// If affected is 0 or 2, it was there already.
1499
	if ($request->affected_rows() != 1)
1500 12
	{
1501
		// Spammer!  You only have to wait a *few* seconds!
1502
		if ($fatal)
1503
		{
1504
			throw new \ElkArte\Exceptions\Exception($error_type . '_WaitTime_broken', false, [$timeLimit]);
1505
		}
1506 12
1507
		return $timeLimit;
1508
	}
1509
1510 12
	// They haven't posted within the limit.
1511
	return false;
1512
}
1513
1514
/**
1515 12
 * A generic function to create a pair of index.php and .htaccess files in a directory
1516 12
 *
1517
 * @param string $path the (absolute) directory path
1518
 * @param bool $allow_localhost if access should be allowed to localhost
1519
 * @param string $files (optional, default '*') parameter for the Files tag
1520
 *
1521 12
 * @return string[]|string|bool on success error string if anything fails
1522 12
 */
1523 12
function secureDirectory($path, $allow_localhost = false, $files = '*')
1524 12
{
1525 12
	if (empty($path))
1526
	{
1527
		return 'empty_path';
1528
	}
1529 12
1530
	if (!FileFunctions::instance()->isWritable($path))
1531
	{
1532
		return 'path_not_writable';
1533
	}
1534
1535
	$directoryname = basename($path);
1536
1537
	// How deep is this from our boarddir
1538
	$tree = explode(DIRECTORY_SEPARATOR, $path);
1539
	$root = explode(DIRECTORY_SEPARATOR, BOARDDIR);
1540
	$count = max(count($tree) - count($root), 0);
1541
1542
	$errors = [];
1543 12
1544
	if (file_exists($path . '/.htaccess'))
1545
	{
1546
		$errors[] = 'htaccess_exists';
1547
	}
1548
	else
1549
	{
1550
		$fh = @fopen($path . '/.htaccess', 'wb');
1551
		if ($fh)
0 ignored issues
show
$fh is of type false|resource, thus it always evaluated to false.
Loading history...
1552
		{
1553
			fwrite($fh, '# Apache 2.4
1554
<IfModule mod_authz_core.c>
1555
	Require all denied
1556
	<Files ' . ($files === '*' ? $files : '~ ' . $files) . '>
1557
		<RequireAll>
1558
			Require all granted
1559
			Require not env blockAccess' . (empty($allow_localhost) ? '
1560
		</RequireAll>
1561
	</Files>' : '
1562
		Require host localhost
1563
		</RequireAll>
1564
	</Files>
1565
1566
	RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . '
1567
</IfModule>
1568
1569
# Apache 2.2
1570
<IfModule !mod_authz_core.c>
1571
	Order Deny,Allow
1572
	Deny from all
1573
1574
	<Files ' . $files . '>
1575
		Allow from all' . (empty($allow_localhost) ? '
1576
	</Files>' : '
1577
		Allow from localhost
1578
	</Files>
1579
1580
	RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . '
1581
</IfModule>');
1582
			fclose($fh);
1583
		}
1584
1585
		$errors[] = 'htaccess_cannot_create_file';
1586
	}
1587
1588
	if (file_exists($path . '/index.php'))
1589
	{
1590
		$errors[] = 'index-php_exists';
1591
	}
1592
	else
1593
	{
1594
		$fh = @fopen($path . '/index.php', 'wb');
1595
		if ($fh)
0 ignored issues
show
$fh is of type false|resource, thus it always evaluated to false.
Loading history...
1596
		{
1597
			fwrite($fh, '<?php
1598
1599
/**
1600
 * This file is here solely to protect your ' . $directoryname . ' directory.
1601
 */
1602
1603
// Look for Settings.php....
1604
if (file_exists(dirname(__FILE__, ' . ($count + 1) . ') . \'/Settings.php\'))
1605
{
1606
	// Found it!
1607
	require(dirname(__FILE__, ' . ($count + 1) . ') . \'/Settings.php\');
1608
	header(\'Location: \' . $boardurl);
1609
}
1610
// Can\'t find it... just forget it.
1611
else
1612
	exit;');
1613
			fclose($fh);
1614
		}
1615
1616
		$errors[] = 'index-php_cannot_create_file';
1617
	}
1618
1619
	if (!empty($errors))
1620
	{
1621
		return $errors;
1622
	}
1623
1624
	return true;
1625
}
1626
1627
/**
1628
 * Helper function that puts together a ban query for a given ip
1629
 *
1630
 * What it does:
1631
 *
1632
 * - Builds the query for ipv6, ipv4 or 255.255.255.255 depending on what's supplied
1633
 *
1634
 * @param string $fullip An IP address either IPv6 or not
1635
 *
1636
 * @return string A SQL condition
1637
 */
1638
function constructBanQueryIP($fullip)
1639
{
1640
	// First attempt a IPv6 address.
1641
	if (isValidIPv6($fullip))
1642
	{
1643
		$ip_parts = convertIPv6toInts($fullip);
1644
1645
		$ban_query = '((' . $ip_parts[0] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1646
			AND (' . $ip_parts[1] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1647
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1648
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low4 AND bi.ip_high4)
1649
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low5 AND bi.ip_high5)
1650
			AND (' . $ip_parts[5] . ' BETWEEN bi.ip_low6 AND bi.ip_high6)
1651
			AND (' . $ip_parts[6] . ' BETWEEN bi.ip_low7 AND bi.ip_high7)
1652
			AND (' . $ip_parts[7] . ' BETWEEN bi.ip_low8 AND bi.ip_high8))';
1653
	}
1654
	// Check if we have a valid IPv4 address.
1655
	elseif (preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $fullip, $ip_parts) == 1)
1656
	{
1657
		$ban_query = '((' . $ip_parts[1] . ' BETWEEN bi.ip_low1 AND bi.ip_high1)
1658
			AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low2 AND bi.ip_high2)
1659
			AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low3 AND bi.ip_high3)
1660
			AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low4 AND bi.ip_high4))';
1661
	}
1662
	// We use '255.255.255.255' for 'unknown' since it's not valid anyway.
1663
	else
1664
	{
1665
		$ban_query = '(bi.ip_low1 = 255 AND bi.ip_high1 = 255
1666
			AND bi.ip_low2 = 255 AND bi.ip_high2 = 255
1667
			AND bi.ip_low3 = 255 AND bi.ip_high3 = 255
1668 2
			AND bi.ip_low4 = 255 AND bi.ip_high4 = 255)';
1669
	}
1670
1671
	return $ban_query;
1672
}
1673
1674
/**
1675
 * Decide if we are going to do any "bad behavior" scanning for this user
1676
 *
1677
 * What it does:
1678
 *
1679
 * - Admins and Moderators get a free pass
1680
 * - Returns true if Accept header is missing
1681
 * - Check with project Honey Pot for known miscreants
1682 2
 *
1683
 * @return bool true if bad, false otherwise
1684 2
 */
1685 2
function runBadBehavior()
1686 2
{
1687 2
	global $modSettings;
1688
1689
	// Admins and Mods get a free pass
1690
	if (!empty(User::$info->is_moderator) || !empty(User::$info->is_admin))
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug Best Practice introduced by
The property is_moderator does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1691
	{
1692
		return false;
1693
	}
1694
1695
	// Clients will have an "Accept" header, generally only bots or scrappers don't
1696
	if (!empty($modSettings['badbehavior_accept_header']) && !array_key_exists('HTTP_ACCEPT', $_SERVER))
1697
	{
1698 2
		return true;
1699
	}
1700
1701
	// Do not block private IP ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 or 127.0.0.0/8
1702
	if (preg_match('~^((10|172\.(1[6-9]|2\d|3[01])|192\.168|127)\.)~', $_SERVER['REMOTE_ADDR']) === 1)
1703
	{
1704
		return false;
1705
	}
1706
1707
	// Project honey pot blacklist check [Your Access Key] [Octet-Reversed IP] [List-Specific Domain]
1708
	if (empty($modSettings['badbehavior_httpbl_key']) || empty($_SERVER['REMOTE_ADDR']))
1709
	{
1710
		return false;
1711
	}
1712
1713
	// Try to load it from the cache first
1714
	$cache = Cache::instance();
1715
	$dnsQuery = $modSettings['badbehavior_httpbl_key'] . '.' . implode('.', array_reverse(explode('.', $_SERVER['REMOTE_ADDR']))) . '.dnsbl.httpbl.org';
1716
	if (!$cache->getVar($dnsResult, 'dnsQuery-' . $_SERVER['REMOTE_ADDR'], 240))
1717
	{
1718
		$dnsResult = gethostbyname($dnsQuery);
1719
		$cache->put('dnsQuery-' . $_SERVER['REMOTE_ADDR'], $dnsResult, 240);
1720
	}
1721
1722
	if (!empty($dnsResult) && $dnsResult !== $dnsQuery)
1723
	{
1724
		$result = explode('.', $dnsResult);
1725
		$result = array_map('intval', $result);
1726
		if ($result[0] === 127 // Valid Response
1727
			&& ($result[3] & 3 || $result[3] & 5) // Listed as Suspicious + Harvester || Suspicious + Comment Spammer
1728
			&& $result[2] >= $modSettings['badbehavior_httpbl_threat'] // Level
1729
			&& $result[1] <= $modSettings['badbehavior_httpbl_maxage']) // Age
1730
		{
1731
			return true;
1732
		}
1733
	}
1734
1735
	return false;
1736
}
1737
1738
/**
1739
 * This protects against brute force attacks on a member's password.
1740
 *
1741
 * What it does:
1742
 *
1743
 * - Importantly, even if the password was right we DON'T TELL THEM!
1744
 * - Allows 5 attempts every 10 seconds
1745
 *
1746
 * @param int $id_member
1747
 * @param string|bool $password_flood_value = false or string joined on |'s
1748
 * @param bool $was_correct = false
1749
 *
1750
 * @throws \ElkArte\Exceptions\Exception no_access
1751
 */
1752
function validatePasswordFlood($id_member, $password_flood_value = false, $was_correct = false)
1753
{
1754
	global $cookiename;
1755
1756
	// As this is only brute protection, we allow 5 attempts every 10 seconds.
1757
1758
	// Destroy any session or cookie data about this member, as they validated wrong.
1759
	require_once(SUBSDIR . '/Auth.subs.php');
1760
	setLoginCookie(-3600, 0);
1761
1762
	if (isset($_SESSION['login_' . $cookiename]))
1763
	{
1764
		unset($_SESSION['login_' . $cookiename]);
1765
	}
1766
1767
	// We need a member!
1768
	if ($id_member === 0)
1769
	{
1770
		// Redirect back!
1771
		redirectexit();
1772
1773
		// Probably not needed, but still make sure...
1774
		throw new \ElkArte\Exceptions\Exception('no_access', false);
1775
	}
1776
1777
	// Let's just initialize to something (and 0 is better than nothing)
1778
	$time_stamp = 0;
1779
	$number_tries = 0;
1780
1781
	// Right, have we got a flood value?
1782
	if ($password_flood_value !== false)
1783
	{
1784
		@[$time_stamp, $number_tries] = explode('|', $password_flood_value);
0 ignored issues
show
It seems like $password_flood_value can also be of type true; however, parameter $string of explode() 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

1784
		@[$time_stamp, $number_tries] = explode('|', /** @scrutinizer ignore-type */ $password_flood_value);
Loading history...
1785
	}
1786
1787
	// Timestamp invalid or non-existent?
1788
	if (empty($number_tries) || $time_stamp < (time() - 10))
1789
	{
1790
		// If it wasn't *that* long ago, don't give them another five goes.
1791
		$number_tries = !empty($number_tries) && $time_stamp < (time() - 20) ? 2 : $number_tries;
1792
		$time_stamp = time();
1793
	}
1794
1795
	$number_tries++;
1796
1797
	// Broken the law?
1798
	if ($number_tries > 5)
1799
	{
1800
		throw new \ElkArte\Exceptions\Exception('login_threshold_brute_fail', 'critical');
1801
	}
1802
1803
	// Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it!
1804
	require_once(SUBSDIR . '/Members.subs.php');
1805
	updateMemberData($id_member, ['passwd_flood' => $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries]);
1806
}
1807
1808
/**
1809
 * This sets the X-Frame-Options header.
1810
 *
1811
 * @param string|null $override the frame option, defaults to deny.
1812
 */
1813
function frameOptionsHeader($override = null)
1814
{
1815
	global $modSettings;
1816
1817
	$option = 'SAMEORIGIN';
1818
1819
	if (is_null($override) && !empty($modSettings['frame_security']))
1820
	{
1821
		$option = $modSettings['frame_security'];
1822
	}
1823
	elseif (in_array($override, ['SAMEORIGIN', 'DENY']))
1824
	{
1825
		$option = $override;
1826
	}
1827
1828
	// Don't bother setting the header if we have disabled it.
1829
	if ($option === 'DISABLE')
1830
	{
1831
		return;
1832
	}
1833
1834
	// Finally set it.
1835
	Headers::instance()->header('X-Frame-Options', $option);
1836
}
1837
1838
/**
1839
 * This adds additional security headers that may prevent browsers from doing something they should not
1840
 *
1841
 * What it does:
1842
 *
1843
 * - X-XSS-Protection header - This header enables the Cross-site scripting (XSS) filter
1844
 * built into most recent web browsers. It's usually enabled by default, so the role of this
1845
 * header is to re-enable the filter for this particular website if it was disabled by the user.
1846
 * - X-Content-Type-Options header - It prevents the browser from doing MIME-type sniffing,
1847
 * only IE and Chrome are honouring this header. This reduces exposure to drive-by download attacks
1848
 * and sites serving user uploaded content that could be treated as executable or dynamic HTML files.
1849
 *
1850
 * @param bool|null $override
1851
 */
1852
function securityOptionsHeader($override = null)
1853
{
1854
	if ($override !== true)
1855
	{
1856
		Headers::instance()
1857
			->header('X-XSS-Protection', '1')
1858
			->header('X-Content-Type-Options', 'nosniff');
1859
	}
1860
}
1861
1862
/**
1863
 * Stop some browsers pre fetching activity to reduce server load
1864
 */
1865
function stop_prefetching()
1866
{
1867
	if ((isset($_SERVER['HTTP_PURPOSE']) && $_SERVER['HTTP_PURPOSE'] === 'prefetch')
1868
		|| (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] === 'prefetch'))
1869
	{
1870
		@ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ob_end_clean(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

1870
		/** @scrutinizer ignore-unhandled */ @ob_end_clean();

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...
1871
		Headers::instance()
1872
			->removeHeader('all')
1873
			->header('X-DNS-Prefetch-Control', 'off')
1874
			->header('Permissions-Policy', 'browsing-topics=(), prefetch-src=()')
1875
			->header('Cache-Control', 'no-store, no-cache, must-revalidate')
1876
			->header('X-Prefetch-Reason', 'Prefetch Forbidden')
1877
			->httpCode(403)
1878
			->sendHeaders();
1879
		die;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1880 24
	}
1881 24
}
1882
1883
/**
1884
 * Check if the admin's session is active
1885
 *
1886
 * @return bool
1887 24
 */
1888
function isAdminSessionActive()
1889
{
1890
	global $modSettings;
1891
1892
	return empty($modSettings['securityDisable']) && (isset($_SESSION['admin_time']) && $_SESSION['admin_time'] + ($modSettings['admin_session_lifetime'] * 60) > time());
1893
}
1894
1895
/**
1896
 * Check if security files exist
1897
 *
1898
 * If files are found, populate $context['security_controls_files']:
1899
 * * 'title'    - $txt['security_risk']
1900
 * * 'errors'    - An array of strings with the key being the filename and the value an error with the filename in it
1901
 *
1902
 * @event integrate_security_files Allows adding / modifying security files array
1903
 *
1904
 * @return bool
1905
 */
1906
function checkSecurityFiles()
1907
{
1908
	global $txt, $context;
1909
1910
	$has_files = false;
1911
1912
	$securityFiles = ['install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~'];
1913
	call_integration_hook('integrate_security_files', [&$securityFiles]);
1914
1915
	foreach ($securityFiles as $securityFile)
1916
	{
1917
		if (file_exists(BOARDDIR . '/' . $securityFile))
1918
		{
1919
			$has_files = true;
1920
1921
			$context['security_controls_files']['title'] = $txt['security_risk'];
1922
			$context['security_controls_files']['errors'][$securityFile] = sprintf($txt['not_removed'], $securityFile);
1923
1924
			if ($securityFile === 'Settings.php~' || $securityFile === 'Settings_bak.php~')
1925
			{
1926
				$context['security_controls_files']['errors'][$securityFile] .= '<span class="smalltext">' . sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)) . '</span>';
1927
			}
1928
		}
1929
	}
1930
1931
	return $has_files;
1932
}
1933
1934
/**
1935
 * The login URL should not redirect to certain areas (attachments, js actions, etc)
1936
 * this function does these checks and return if the URL is valid or not.
1937
 *
1938
 * @param string $url - The URL to validate
1939
 * @param bool $match_board - If true tries to match board|topic in the URL as well
1940
 * @return bool
1941
 */
1942
function validLoginUrl($url, $match_board = false)
1943
{
1944
	if (empty($url))
1945
	{
1946
		return false;
1947
	}
1948
1949
	if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://'))
1950 2
	{
1951
		return false;
1952
	}
1953
1954
	$invalid_strings = ['dlattach' => '~(board|topic)[=,]~', 'jslocale' => '', 'login' => ''];
1955 2
	call_integration_hook('integrate_validLoginUrl', [&$invalid_strings]);
1956
1957
	foreach ($invalid_strings as $invalid_string => $valid_match)
1958
	{
1959
		if (str_contains($url, $invalid_string)
1960 2
			|| ($match_board === true && !empty($valid_match) && preg_match($valid_match, $url) !== 1))
1961 2
		{
1962
			return false;
1963 2
		}
1964
	}
1965 2
1966
	return true;
1967
}
1968