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); |
164
|
|
|
|
165
|
|
|
// Password correct? |
166
|
|
|
if ($good_password || validateLoginPassword($password, User::$info->passwd, $hash ? '' : User::$info->username)) |
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) |
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'])) |
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) |
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 |
287
|
|
|
|| $_SESSION['ban']['ip'] !== User::$info->ip |
288
|
|
|
|| $_SESSION['ban']['ip2'] !== User::$info->ip2 |
289
|
|
|
|| (isset(User::$info->email) && $_SESSION['ban']['email'] !== User::$info->email)) |
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)) |
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)) |
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']; |
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) |
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 = []; |
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)) |
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); |
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)) |
572
|
|
|
{ |
573
|
|
|
unset($permission_change[$old]); |
574
|
|
|
} |
575
|
|
|
else |
576
|
|
|
{ |
577
|
|
|
User::$info->permissions = array_merge((array) User::$info->permissions, $new); |
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) |
587
|
|
|
{ |
588
|
|
|
User::$info->mod_cache = $_SESSION['mc']; |
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, |
648
|
|
|
User::$info->ip, |
649
|
|
|
$email ?? (string) User::$info->email, |
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 (strpos($_SERVER['HTTP_HOST'], ':') !== false) |
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(); |
891
|
|
|
Headers::instance() |
892
|
|
|
->removeHeader('all') |
893
|
|
|
->headerSpecial('HTTP/1.1 403 Forbidden - Session timeout') |
894
|
|
|
->sendHeaders(); |
895
|
|
|
die; |
896
|
|
|
} |
897
|
|
|
throw new \ElkArte\Exceptions\Exception($error, isset($log_error) ? 'user' : false, $sprintf ?? []); |
898
|
|
|
} |
899
|
|
|
// A session error occurred, return the error to the calling function. |
900
|
|
|
else |
901
|
|
|
{ |
902
|
|
|
return $error; |
903
|
|
|
} |
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); |
|
|
|
|
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
|
|
|
else |
1117
|
|
|
{ |
1118
|
|
|
return false; |
1119
|
|
|
} |
1120
|
|
|
} |
1121
|
|
|
// Don't check, just free the stack number. |
1122
|
|
|
elseif ($action === 'free' && isset($_REQUEST['seqnum']) && in_array($_REQUEST['seqnum'], $_SESSION['forms'], true)) |
1123
|
|
|
{ |
1124
|
|
|
$_SESSION['forms'] = array_diff($_SESSION['forms'], [$_REQUEST['seqnum']]); |
1125
|
|
|
} |
1126
|
|
|
elseif ($action !== 'free') |
1127
|
6 |
|
{ |
1128
|
|
|
trigger_error("checkSubmitOnce(): Invalid action '" . $action . "'", E_USER_WARNING); |
1129
|
|
|
} |
1130
|
|
|
} |
1131
|
|
|
|
1132
|
|
|
/** |
1133
|
|
|
* This function checks whether the user is allowed to do permission. (ie. post_new.) |
1134
|
|
|
* |
1135
|
|
|
* What it does: |
1136
|
|
|
* |
1137
|
|
|
* - If boards parameter is specified, checks those boards instead of the current one (if applicable). |
1138
|
|
|
* - Always returns true if the user is an administrator. |
1139
|
|
|
* |
1140
|
|
|
* @param string[]|string $permission permission |
1141
|
|
|
* @param int[]|int|null $boards array of board IDs, a single id or null |
1142
|
|
|
* |
1143
|
|
|
* @return bool if the user can do the permission |
1144
|
|
|
*/ |
1145
|
330 |
|
function allowedTo($permission, $boards = null) |
1146
|
|
|
{ |
1147
|
|
|
$db = database(); |
1148
|
330 |
|
|
1149
|
|
|
// You're always allowed to do nothing. (unless you're a working man, MR. LAZY :P!) |
1150
|
12 |
|
if (empty($permission)) |
1151
|
|
|
{ |
1152
|
|
|
return true; |
1153
|
|
|
} |
1154
|
330 |
|
|
1155
|
|
|
// You're never allowed to do something if your data hasn't been loaded yet! |
1156
|
|
|
if (empty(User::$info) || !isset(User::$info['permissions'])) |
1157
|
|
|
{ |
1158
|
|
|
return false; |
1159
|
|
|
} |
1160
|
330 |
|
|
1161
|
|
|
// Administrators are supermen :P. |
1162
|
306 |
|
if (User::$info->is_admin) |
1163
|
|
|
{ |
1164
|
|
|
return true; |
1165
|
|
|
} |
1166
|
24 |
|
|
1167
|
|
|
// Make sure permission is a valid array |
1168
|
22 |
|
if (!is_array($permission)) |
1169
|
|
|
{ |
1170
|
|
|
$permission = [$permission]; |
1171
|
|
|
} |
1172
|
24 |
|
|
1173
|
|
|
// Are we checking the _current_ board, or some other boards? |
1174
|
24 |
|
if ($boards === null) |
1175
|
|
|
{ |
1176
|
12 |
|
if (empty(User::$info->permissions)) |
1177
|
|
|
{ |
1178
|
|
|
return false; |
1179
|
|
|
} |
1180
|
12 |
|
|
1181
|
|
|
// Check if they can do it, you aren't allowed, by default. |
1182
|
|
|
return array_intersect($permission, User::$info->permissions) !== []; |
1183
|
|
|
} |
1184
|
|
|
|
1185
|
|
|
if (!is_array($boards)) |
1186
|
|
|
{ |
1187
|
|
|
$boards = [$boards]; |
1188
|
|
|
} |
1189
|
|
|
|
1190
|
|
|
if (empty(User::$info->groups)) |
1191
|
|
|
{ |
1192
|
|
|
return false; |
1193
|
|
|
} |
1194
|
|
|
|
1195
|
|
|
$request = $db->query('', ' |
1196
|
|
|
SELECT |
1197
|
|
|
MIN(bp.add_deny) AS add_deny |
1198
|
|
|
FROM {db_prefix}boards AS b |
1199
|
|
|
INNER JOIN {db_prefix}board_permissions AS bp ON (bp.id_profile = b.id_profile) |
1200
|
|
|
LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member}) |
1201
|
|
|
WHERE b.id_board IN ({array_int:board_list}) |
1202
|
|
|
AND bp.id_group IN ({array_int:group_list}, {int:moderator_group}) |
1203
|
|
|
AND bp.permission IN ({array_string:permission_list}) |
1204
|
|
|
AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group}) |
1205
|
|
|
GROUP BY b.id_board', |
1206
|
|
|
[ |
1207
|
|
|
'current_member' => User::$info->id, |
1208
|
|
|
'board_list' => $boards, |
1209
|
|
|
'group_list' => User::$info->groups, |
1210
|
|
|
'moderator_group' => 3, |
1211
|
|
|
'permission_list' => $permission, |
1212
|
|
|
] |
1213
|
|
|
); |
1214
|
|
|
|
1215
|
|
|
// Make sure they can do it on all the boards. |
1216
|
|
|
if ($request->num_rows() !== count($boards)) |
1217
|
|
|
{ |
1218
|
|
|
return false; |
1219
|
|
|
} |
1220
|
|
|
|
1221
|
|
|
$result = true; |
1222
|
|
|
while (($row = $request->fetch_assoc())) |
1223
|
|
|
{ |
1224
|
|
|
$result = $result && !empty($row['add_deny']); |
1225
|
|
|
} |
1226
|
|
|
|
1227
|
|
|
$request->free_result(); |
1228
|
|
|
|
1229
|
|
|
// If the query returned 1, they can do it... otherwise, they can't. |
1230
|
|
|
return $result; |
1231
|
|
|
} |
1232
|
|
|
|
1233
|
|
|
/** |
1234
|
|
|
* This function returns fatal error if the user doesn't have the respective permission. |
1235
|
|
|
* |
1236
|
|
|
* What it does: |
1237
|
|
|
* |
1238
|
|
|
* - Uses allowedTo() to check if the user is allowed to do permission. |
1239
|
|
|
* - Checks the passed boards or current board for the permission. |
1240
|
|
|
* - If they are not, it loads the Errors language file and shows an error using $txt['cannot_' . $permission]. |
1241
|
|
|
* - If they are a guest and cannot do it, this calls is_not_guest(). |
1242
|
|
|
* |
1243
|
|
|
* @param string[]|string $permission array of or single string, of permissions to check |
1244
|
|
|
* @param int[]|null $boards = null |
1245
|
|
|
* |
1246
|
|
|
* @throws \ElkArte\Exceptions\Exception cannot_xyz where xyz is the permission |
1247
|
68 |
|
*/ |
1248
|
|
|
function isAllowedTo($permission, $boards = null) |
1249
|
68 |
|
{ |
1250
|
|
|
global $txt; |
1251
|
|
|
|
1252
|
|
|
static $heavy_permissions = [ |
1253
|
|
|
'admin_forum', |
1254
|
|
|
'manage_attachments', |
1255
|
|
|
'manage_smileys', |
1256
|
|
|
'manage_boards', |
1257
|
|
|
'edit_news', |
1258
|
|
|
'moderate_forum', |
1259
|
|
|
'manage_bans', |
1260
|
|
|
'manage_membergroups', |
1261
|
|
|
'manage_permissions', |
1262
|
68 |
|
]; |
1263
|
|
|
|
1264
|
|
|
// Make it an array, even if a string was passed. |
1265
|
68 |
|
$permission = is_array($permission) ? $permission : [$permission]; |
1266
|
|
|
|
1267
|
|
|
// Check the permission and return an error... |
1268
|
|
|
if (!allowedTo($permission, $boards)) |
1269
|
|
|
{ |
1270
|
|
|
// Pick the last array entry as the permission shown as the error. |
1271
|
|
|
$error_permission = array_shift($permission); |
1272
|
|
|
|
1273
|
|
|
// If they are a guest, show a login. (because the error might be gone if they do!) |
1274
|
|
|
if (User::$info->is_guest) |
1275
|
|
|
{ |
1276
|
|
|
Txt::load('Errors'); |
1277
|
|
|
is_not_guest($txt['cannot_' . $error_permission]); |
1278
|
|
|
} |
1279
|
|
|
|
1280
|
|
|
// Clear the action because they aren't really doing that! |
1281
|
|
|
$_GET['action'] = ''; |
1282
|
|
|
$_GET['board'] = ''; |
1283
|
|
|
$_GET['topic'] = ''; |
1284
|
|
|
writeLog(true); |
1285
|
|
|
|
1286
|
|
|
throw new \ElkArte\Exceptions\Exception('cannot_' . $error_permission, false); |
1287
|
|
|
} |
1288
|
68 |
|
|
1289
|
|
|
// If you're doing something on behalf of some "heavy" permissions, validate your session. |
1290
|
|
|
// (take out the heavy permissions, and if you can't do anything but those, you need a validated session.) |
1291
|
|
|
if (!allowedTo(array_diff($permission, $heavy_permissions), $boards)) |
1292
|
68 |
|
{ |
1293
|
|
|
validateSession(); |
1294
|
|
|
} |
1295
|
|
|
} |
1296
|
|
|
|
1297
|
|
|
/** |
1298
|
|
|
* Return the boards a user has a certain (board) permission on. (array(0) if all.) |
1299
|
|
|
* |
1300
|
|
|
* What it does: |
1301
|
|
|
* |
1302
|
|
|
* - Returns a list of boards on which the user is allowed to do the specified permission. |
1303
|
|
|
* - Returns an array with only a 0 in it if the user has permission to do this on every board. |
1304
|
|
|
* - Returns an empty array if he or she cannot do this on any board. |
1305
|
|
|
* - If check_access is true will also make sure the group has proper access to that board. |
1306
|
|
|
* |
1307
|
|
|
* @param string[]|string $permissions array of permission names to check access against |
1308
|
|
|
* @param bool $check_access = true |
1309
|
|
|
* @param bool $simple = true Set $simple to true to use this function in compatibility mode |
1310
|
|
|
* otherwise, the resultant array becomes split into the multiple |
1311
|
|
|
* permissions that were passed. Other than that, it's just the normal |
1312
|
|
|
* state of play that you're used to. |
1313
|
|
|
* |
1314
|
|
|
* @return int[] |
1315
|
|
|
* @throws \ElkArte\Exceptions\Exception |
1316
|
3 |
|
*/ |
1317
|
|
|
function boardsAllowedTo($permissions, $check_access = true, $simple = true) |
1318
|
|
|
{ |
1319
|
3 |
|
$db = database(); |
1320
|
|
|
|
1321
|
3 |
|
// Arrays are nice, most of the time. |
1322
|
|
|
if (!is_array($permissions)) |
1323
|
|
|
{ |
1324
|
|
|
$permissions = [$permissions]; |
1325
|
3 |
|
} |
1326
|
|
|
|
1327
|
2 |
|
// I am the master, the master of the universe! |
1328
|
|
|
if (User::$info->is_admin) |
1329
|
2 |
|
{ |
1330
|
|
|
if ($simple) |
1331
|
|
|
{ |
1332
|
|
|
return [0]; |
1333
|
|
|
} |
1334
|
|
|
|
1335
|
|
|
$boards = []; |
1336
|
|
|
foreach ($permissions as $permission) |
1337
|
|
|
{ |
1338
|
|
|
$boards[$permission] = [0]; |
1339
|
|
|
} |
1340
|
|
|
|
1341
|
|
|
return $boards; |
1342
|
|
|
} |
1343
|
|
|
|
1344
|
1 |
|
// All groups the user is in except 'moderator'. |
1345
|
|
|
$groups = array_diff(User::$info->groups, [3]); |
1346
|
1 |
|
|
1347
|
1 |
|
$boards = []; |
1348
|
1 |
|
$deny_boards = []; |
1349
|
|
|
$db->fetchQuery(' |
1350
|
1 |
|
SELECT |
1351
|
|
|
b.id_board, bp.add_deny' . ($simple ? '' : ', bp.permission') . ' |
1352
|
|
|
FROM {db_prefix}board_permissions AS bp |
1353
|
|
|
INNER JOIN {db_prefix}boards AS b ON (b.id_profile = bp.id_profile) |
1354
|
|
|
LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board AND mods.id_member = {int:current_member}) |
1355
|
|
|
WHERE bp.id_group IN ({array_int:group_list}, {int:moderator_group}) |
1356
|
|
|
AND bp.permission IN ({array_string:permissions}) |
1357
|
1 |
|
AND (mods.id_member IS NOT NULL OR bp.id_group != {int:moderator_group})' . |
1358
|
|
|
($check_access ? ' AND {query_see_board}' : ''), |
1359
|
1 |
|
[ |
1360
|
1 |
|
'current_member' => User::$info->id, |
1361
|
1 |
|
'group_list' => $groups, |
1362
|
1 |
|
'moderator_group' => 3, |
1363
|
|
|
'permissions' => $permissions, |
1364
|
1 |
|
] |
1365
|
|
|
)->fetch_callback( |
1366
|
|
|
static function ($row) use ($simple, &$deny_boards, &$boards) { |
1367
|
|
|
if ($simple) |
1368
|
|
|
{ |
1369
|
|
|
if (empty($row['add_deny'])) |
1370
|
|
|
{ |
1371
|
|
|
$deny_boards[] = (int) $row['id_board']; |
1372
|
|
|
} |
1373
|
|
|
else |
1374
|
|
|
{ |
1375
|
|
|
$boards[] = (int) $row['id_board']; |
1376
|
|
|
} |
1377
|
|
|
} |
1378
|
|
|
elseif (empty($row['add_deny'])) |
1379
|
|
|
{ |
1380
|
|
|
$deny_boards[$row['permission']][] = (int) $row['id_board']; |
1381
|
|
|
} |
1382
|
|
|
else |
1383
|
|
|
{ |
1384
|
|
|
$boards[$row['permission']][] = (int) $row['id_board']; |
1385
|
1 |
|
} |
1386
|
|
|
} |
1387
|
|
|
); |
1388
|
1 |
|
|
1389
|
|
|
if ($simple) |
1390
|
1 |
|
{ |
1391
|
|
|
$boards = array_unique(array_values(array_diff($boards, $deny_boards))); |
1392
|
|
|
} |
1393
|
|
|
else |
1394
|
|
|
{ |
1395
|
|
|
foreach ($permissions as $permission) |
1396
|
|
|
{ |
1397
|
|
|
// Never had it to start with |
1398
|
|
|
if (empty($boards[$permission])) |
1399
|
|
|
{ |
1400
|
|
|
$boards[$permission] = []; |
1401
|
|
|
} |
1402
|
|
|
else |
1403
|
|
|
{ |
1404
|
|
|
// Or it may have been removed |
1405
|
|
|
$deny_boards[$permission] = $deny_boards[$permission] ?? []; |
1406
|
|
|
$boards[$permission] = array_unique(array_values(array_diff($boards[$permission], $deny_boards[$permission]))); |
1407
|
|
|
} |
1408
|
|
|
} |
1409
|
|
|
} |
1410
|
1 |
|
|
1411
|
|
|
return $boards; |
1412
|
|
|
} |
1413
|
|
|
|
1414
|
|
|
/** |
1415
|
|
|
* Returns whether an email address should be shown and how. |
1416
|
|
|
* |
1417
|
|
|
* What it does: |
1418
|
|
|
* |
1419
|
|
|
* Possible outcomes are: |
1420
|
|
|
* If it's your own profile yes. |
1421
|
|
|
* If you're a moderator with sufficient permissions: yes. |
1422
|
|
|
* Otherwise: no |
1423
|
|
|
* |
1424
|
|
|
* @param int $userProfile_id |
1425
|
|
|
* |
1426
|
|
|
* @return bool |
1427
|
|
|
*/ |
1428
|
|
|
function showEmailAddress($userProfile_id) |
1429
|
|
|
{ |
1430
|
|
|
// Should this user's email address be shown? |
1431
|
|
|
if ((User::$info->is_guest === false && User::$info->id === (int) $userProfile_id)) |
1432
|
|
|
{ |
1433
|
|
|
return true; |
1434
|
|
|
} |
1435
|
|
|
|
1436
|
|
|
if (allowedTo('moderate_forum')) |
1437
|
|
|
{ |
1438
|
|
|
return true; |
1439
|
|
|
} |
1440
|
|
|
|
1441
|
8 |
|
return false; |
1442
|
|
|
} |
1443
|
|
|
|
1444
|
|
|
/** |
1445
|
|
|
* This function attempts to protect from carrying out specific actions repeatedly. |
1446
|
8 |
|
* |
1447
|
|
|
* What it does: |
1448
|
6 |
|
* |
1449
|
|
|
* - Checks if a user is trying specific actions faster than a given minimum wait threshold. |
1450
|
|
|
* - The time taken depends on error_type - generally uses the modSetting. |
1451
|
2 |
|
* - Generates a fatal message when triggered, suspending execution. |
1452
|
|
|
* |
1453
|
2 |
|
* @event integrate_spam_protection Allows updating action wait timeOverrides |
1454
|
|
|
* @param string $error_type used also as a $txt index. (not an actual string.) |
1455
|
|
|
* @param bool $fatal is the spam check a fatal error on failure |
1456
|
|
|
* |
1457
|
|
|
* @return bool|int|mixed |
1458
|
|
|
* @throws \ElkArte\Exceptions\Exception |
1459
|
|
|
*/ |
1460
|
|
|
function spamProtection($error_type, $fatal = true) |
1461
|
|
|
{ |
1462
|
|
|
global $modSettings; |
1463
|
|
|
|
1464
|
|
|
$db = database(); |
1465
|
|
|
|
1466
|
|
|
// Certain types take less/more time. |
1467
|
|
|
$timeOverrides = [ |
1468
|
|
|
'login' => 2, |
1469
|
|
|
'register' => 2, |
1470
|
|
|
'remind' => 30, |
1471
|
|
|
'contact' => 30, |
1472
|
|
|
'sendmail' => $modSettings['spamWaitTime'] * 5, |
1473
|
|
|
'reporttm' => $modSettings['spamWaitTime'] * 4, |
1474
|
|
|
'search' => empty($modSettings['search_floodcontrol_time']) ? 1 : $modSettings['search_floodcontrol_time'], |
1475
|
|
|
]; |
1476
|
|
|
call_integration_hook('integrate_spam_protection', [&$timeOverrides]); |
1477
|
|
|
|
1478
|
|
|
// Moderators are free... |
1479
|
|
|
$timeLimit = allowedTo('moderate_board') ? 2 : $timeOverrides[$error_type] ?? $modSettings['spamWaitTime']; |
1480
|
|
|
|
1481
|
|
|
// Delete old entries... |
1482
|
12 |
|
$db->query('', ' |
1483
|
|
|
DELETE FROM {db_prefix}log_floodcontrol |
1484
|
12 |
|
WHERE log_time < {int:log_time} |
1485
|
|
|
AND log_type = {string:log_type}', |
1486
|
|
|
[ |
1487
|
|
|
'log_time' => time() - $timeLimit, |
1488
|
12 |
|
'log_type' => $error_type, |
1489
|
12 |
|
] |
1490
|
12 |
|
); |
1491
|
12 |
|
|
1492
|
12 |
|
// Add a new entry, deleting the old if necessary. |
1493
|
12 |
|
$request = $db->replace( |
1494
|
12 |
|
'{db_prefix}log_floodcontrol', |
1495
|
12 |
|
['ip' => 'string-16', 'log_time' => 'int', 'log_type' => 'string'], |
1496
|
|
|
[User::$info->ip, time(), $error_type], |
1497
|
12 |
|
['ip', 'log_type'] |
1498
|
|
|
); |
1499
|
|
|
|
1500
|
12 |
|
// If affected is 0 or 2, it was there already. |
1501
|
|
|
if ($request->affected_rows() != 1) |
1502
|
|
|
{ |
1503
|
|
|
// Spammer! You only have to wait a *few* seconds! |
1504
|
|
|
if ($fatal) |
1505
|
|
|
{ |
1506
|
12 |
|
throw new \ElkArte\Exceptions\Exception($error_type . '_WaitTime_broken', false, [$timeLimit]); |
1507
|
|
|
} |
1508
|
|
|
|
1509
|
|
|
return $timeLimit; |
1510
|
12 |
|
} |
1511
|
|
|
|
1512
|
|
|
// They haven't posted within the limit. |
1513
|
|
|
return false; |
1514
|
|
|
} |
1515
|
12 |
|
|
1516
|
12 |
|
/** |
1517
|
|
|
* A generic function to create a pair of index.php and .htaccess files in a directory |
1518
|
|
|
* |
1519
|
|
|
* @param string $path the (absolute) directory path |
1520
|
|
|
* @param bool $allow_localhost if access should be allowed to localhost |
1521
|
12 |
|
* @param string $files (optional, default '*') parameter for the Files tag |
1522
|
12 |
|
* |
1523
|
12 |
|
* @return string[]|string|bool on success error string if anything fails |
1524
|
12 |
|
*/ |
1525
|
12 |
|
function secureDirectory($path, $allow_localhost = false, $files = '*') |
1526
|
|
|
{ |
1527
|
|
|
if (empty($path)) |
1528
|
|
|
{ |
1529
|
12 |
|
return 'empty_path'; |
1530
|
|
|
} |
1531
|
|
|
|
1532
|
|
|
if (!FileFunctions::instance()->isWritable($path)) |
1533
|
|
|
{ |
1534
|
|
|
return 'path_not_writable'; |
1535
|
|
|
} |
1536
|
|
|
|
1537
|
|
|
$directoryname = basename($path); |
1538
|
|
|
|
1539
|
|
|
// How deep is this from our boarddir |
1540
|
|
|
$tree = explode(DIRECTORY_SEPARATOR, $path); |
1541
|
|
|
$root = explode(DIRECTORY_SEPARATOR, BOARDDIR); |
1542
|
|
|
$count = max(count($tree) - count($root), 0); |
1543
|
12 |
|
|
1544
|
|
|
$errors = []; |
1545
|
|
|
|
1546
|
|
|
if (file_exists($path . '/.htaccess')) |
1547
|
|
|
{ |
1548
|
|
|
$errors[] = 'htaccess_exists'; |
1549
|
|
|
} |
1550
|
|
|
else |
1551
|
|
|
{ |
1552
|
|
|
$fh = @fopen($path . '/.htaccess', 'wb'); |
1553
|
|
|
if ($fh) |
1554
|
|
|
{ |
1555
|
|
|
fwrite($fh, '# Apache 2.4 |
1556
|
|
|
<IfModule mod_authz_core.c> |
1557
|
|
|
Require all denied |
1558
|
|
|
<Files ' . ($files === '*' ? $files : '~ ' . $files) . '> |
1559
|
|
|
<RequireAll> |
1560
|
|
|
Require all granted |
1561
|
|
|
Require not env blockAccess' . (empty($allow_localhost) ? ' |
1562
|
|
|
</RequireAll> |
1563
|
|
|
</Files>' : ' |
1564
|
|
|
Require host localhost |
1565
|
|
|
</RequireAll> |
1566
|
|
|
</Files> |
1567
|
|
|
|
1568
|
|
|
RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . ' |
1569
|
|
|
</IfModule> |
1570
|
|
|
|
1571
|
|
|
# Apache 2.2 |
1572
|
|
|
<IfModule !mod_authz_core.c> |
1573
|
|
|
Order Deny,Allow |
1574
|
|
|
Deny from all |
1575
|
|
|
|
1576
|
|
|
<Files ' . $files . '> |
1577
|
|
|
Allow from all' . (empty($allow_localhost) ? ' |
1578
|
|
|
</Files>' : ' |
1579
|
|
|
Allow from localhost |
1580
|
|
|
</Files> |
1581
|
|
|
|
1582
|
|
|
RemoveHandler .php .php3 .phtml .cgi .fcgi .pl .fpl .shtml') . ' |
1583
|
|
|
</IfModule>'); |
1584
|
|
|
fclose($fh); |
1585
|
|
|
} |
1586
|
|
|
|
1587
|
|
|
$errors[] = 'htaccess_cannot_create_file'; |
1588
|
|
|
} |
1589
|
|
|
|
1590
|
|
|
if (file_exists($path . '/index.php')) |
1591
|
|
|
{ |
1592
|
|
|
$errors[] = 'index-php_exists'; |
1593
|
|
|
} |
1594
|
|
|
else |
1595
|
|
|
{ |
1596
|
|
|
$fh = @fopen($path . '/index.php', 'wb'); |
1597
|
|
|
if ($fh) |
1598
|
|
|
{ |
1599
|
|
|
fwrite($fh, '<?php |
1600
|
|
|
|
1601
|
|
|
/** |
1602
|
|
|
* This file is here solely to protect your ' . $directoryname . ' directory. |
1603
|
|
|
*/ |
1604
|
|
|
|
1605
|
|
|
// Look for Settings.php.... |
1606
|
|
|
if (file_exists(dirname(__FILE__, ' . ($count + 1) . ') . \'/Settings.php\')) |
1607
|
|
|
{ |
1608
|
|
|
// Found it! |
1609
|
|
|
require(dirname(__FILE__, ' . ($count + 1) . ') . \'/Settings.php\'); |
1610
|
|
|
header(\'Location: \' . $boardurl); |
1611
|
|
|
} |
1612
|
|
|
// Can\'t find it... just forget it. |
1613
|
|
|
else |
1614
|
|
|
exit;'); |
1615
|
|
|
fclose($fh); |
1616
|
|
|
} |
1617
|
|
|
|
1618
|
|
|
$errors[] = 'index-php_cannot_create_file'; |
1619
|
|
|
} |
1620
|
|
|
|
1621
|
|
|
if (!empty($errors)) |
1622
|
|
|
{ |
1623
|
|
|
return $errors; |
1624
|
|
|
} |
1625
|
|
|
|
1626
|
|
|
return true; |
1627
|
|
|
} |
1628
|
|
|
|
1629
|
|
|
/** |
1630
|
|
|
* Helper function that puts together a ban query for a given ip |
1631
|
|
|
* |
1632
|
|
|
* What it does: |
1633
|
|
|
* |
1634
|
|
|
* - Builds the query for ipv6, ipv4 or 255.255.255.255 depending on what's supplied |
1635
|
|
|
* |
1636
|
|
|
* @param string $fullip An IP address either IPv6 or not |
1637
|
|
|
* |
1638
|
|
|
* @return string A SQL condition |
1639
|
|
|
*/ |
1640
|
|
|
function constructBanQueryIP($fullip) |
1641
|
|
|
{ |
1642
|
|
|
// First attempt a IPv6 address. |
1643
|
|
|
if (isValidIPv6($fullip)) |
1644
|
|
|
{ |
1645
|
|
|
$ip_parts = convertIPv6toInts($fullip); |
1646
|
|
|
|
1647
|
|
|
$ban_query = '((' . $ip_parts[0] . ' BETWEEN bi.ip_low1 AND bi.ip_high1) |
1648
|
|
|
AND (' . $ip_parts[1] . ' BETWEEN bi.ip_low2 AND bi.ip_high2) |
1649
|
|
|
AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low3 AND bi.ip_high3) |
1650
|
|
|
AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low4 AND bi.ip_high4) |
1651
|
|
|
AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low5 AND bi.ip_high5) |
1652
|
|
|
AND (' . $ip_parts[5] . ' BETWEEN bi.ip_low6 AND bi.ip_high6) |
1653
|
|
|
AND (' . $ip_parts[6] . ' BETWEEN bi.ip_low7 AND bi.ip_high7) |
1654
|
|
|
AND (' . $ip_parts[7] . ' BETWEEN bi.ip_low8 AND bi.ip_high8))'; |
1655
|
|
|
} |
1656
|
|
|
// Check if we have a valid IPv4 address. |
1657
|
|
|
elseif (preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $fullip, $ip_parts) == 1) |
1658
|
|
|
{ |
1659
|
|
|
$ban_query = '((' . $ip_parts[1] . ' BETWEEN bi.ip_low1 AND bi.ip_high1) |
1660
|
|
|
AND (' . $ip_parts[2] . ' BETWEEN bi.ip_low2 AND bi.ip_high2) |
1661
|
|
|
AND (' . $ip_parts[3] . ' BETWEEN bi.ip_low3 AND bi.ip_high3) |
1662
|
|
|
AND (' . $ip_parts[4] . ' BETWEEN bi.ip_low4 AND bi.ip_high4))'; |
1663
|
|
|
} |
1664
|
|
|
// We use '255.255.255.255' for 'unknown' since it's not valid anyway. |
1665
|
|
|
else |
1666
|
|
|
{ |
1667
|
|
|
$ban_query = '(bi.ip_low1 = 255 AND bi.ip_high1 = 255 |
1668
|
2 |
|
AND bi.ip_low2 = 255 AND bi.ip_high2 = 255 |
1669
|
|
|
AND bi.ip_low3 = 255 AND bi.ip_high3 = 255 |
1670
|
|
|
AND bi.ip_low4 = 255 AND bi.ip_high4 = 255)'; |
1671
|
|
|
} |
1672
|
|
|
|
1673
|
|
|
return $ban_query; |
1674
|
|
|
} |
1675
|
|
|
|
1676
|
|
|
/** |
1677
|
|
|
* Decide if we are going to do any "bad behavior" scanning for this user |
1678
|
|
|
* |
1679
|
|
|
* What it does: |
1680
|
|
|
* |
1681
|
|
|
* - Admins and Moderators get a free pass |
1682
|
2 |
|
* - Returns true if Accept header is missing |
1683
|
|
|
* - Check with project Honey Pot for known miscreants |
1684
|
2 |
|
* |
1685
|
2 |
|
* @return bool true if bad, false otherwise |
1686
|
2 |
|
*/ |
1687
|
2 |
|
function runBadBehavior() |
1688
|
|
|
{ |
1689
|
|
|
global $modSettings; |
1690
|
|
|
|
1691
|
|
|
// Admins and Mods get a free pass |
1692
|
|
|
if (!empty(User::$info->is_moderator) || !empty(User::$info->is_admin)) |
1693
|
|
|
{ |
1694
|
|
|
return false; |
1695
|
|
|
} |
1696
|
|
|
|
1697
|
|
|
// Clients will have an "Accept" header, generally only bots or scrappers don't |
1698
|
2 |
|
if (!empty($modSettings['badbehavior_accept_header']) && !array_key_exists('HTTP_ACCEPT', $_SERVER)) |
1699
|
|
|
{ |
1700
|
|
|
return true; |
1701
|
|
|
} |
1702
|
|
|
|
1703
|
|
|
// 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 |
1704
|
|
|
if (preg_match('~^((10|172\.(1[6-9]|2\d|3[01])|192\.168|127)\.)~', $_SERVER['REMOTE_ADDR']) === 1) |
1705
|
|
|
{ |
1706
|
|
|
return false; |
1707
|
|
|
} |
1708
|
|
|
|
1709
|
|
|
// Project honey pot blacklist check [Your Access Key] [Octet-Reversed IP] [List-Specific Domain] |
1710
|
|
|
if (empty($modSettings['badbehavior_httpbl_key']) || empty($_SERVER['REMOTE_ADDR'])) |
1711
|
|
|
{ |
1712
|
|
|
return false; |
1713
|
|
|
} |
1714
|
|
|
|
1715
|
|
|
// Try to load it from the cache first |
1716
|
|
|
$cache = Cache::instance(); |
1717
|
|
|
$dnsQuery = $modSettings['badbehavior_httpbl_key'] . '.' . implode('.', array_reverse(explode('.', $_SERVER['REMOTE_ADDR']))) . '.dnsbl.httpbl.org'; |
1718
|
|
|
if (!$cache->getVar($dnsResult, 'dnsQuery-' . $_SERVER['REMOTE_ADDR'], 240)) |
1719
|
|
|
{ |
1720
|
|
|
$dnsResult = gethostbyname($dnsQuery); |
1721
|
|
|
$cache->put('dnsQuery-' . $_SERVER['REMOTE_ADDR'], $dnsResult, 240); |
1722
|
|
|
} |
1723
|
|
|
|
1724
|
|
|
if (!empty($dnsResult) && $dnsResult !== $dnsQuery) |
1725
|
|
|
{ |
1726
|
|
|
$result = explode('.', $dnsResult); |
1727
|
|
|
$result = array_map('intval', $result); |
1728
|
|
|
if ($result[0] === 127 // Valid Response |
1729
|
|
|
&& ($result[3] & 3 || $result[3] & 5) // Listed as Suspicious + Harvester || Suspicious + Comment Spammer |
1730
|
|
|
&& $result[2] >= $modSettings['badbehavior_httpbl_threat'] // Level |
1731
|
|
|
&& $result[1] <= $modSettings['badbehavior_httpbl_maxage']) // Age |
1732
|
|
|
{ |
1733
|
|
|
return true; |
1734
|
|
|
} |
1735
|
|
|
} |
1736
|
|
|
|
1737
|
|
|
return false; |
1738
|
|
|
} |
1739
|
|
|
|
1740
|
|
|
/** |
1741
|
|
|
* This protects against brute force attacks on a member's password. |
1742
|
|
|
* |
1743
|
|
|
* What it does: |
1744
|
|
|
* |
1745
|
|
|
* - Importantly, even if the password was right we DON'T TELL THEM! |
1746
|
|
|
* - Allows 5 attempts every 10 seconds |
1747
|
|
|
* |
1748
|
|
|
* @param int $id_member |
1749
|
|
|
* @param string|bool $password_flood_value = false or string joined on |'s |
1750
|
|
|
* @param bool $was_correct = false |
1751
|
|
|
* |
1752
|
|
|
* @throws \ElkArte\Exceptions\Exception no_access |
1753
|
|
|
*/ |
1754
|
|
|
function validatePasswordFlood($id_member, $password_flood_value = false, $was_correct = false) |
1755
|
|
|
{ |
1756
|
|
|
global $cookiename; |
1757
|
|
|
|
1758
|
|
|
// As this is only brute protection, we allow 5 attempts every 10 seconds. |
1759
|
|
|
|
1760
|
|
|
// Destroy any session or cookie data about this member, as they validated wrong. |
1761
|
|
|
require_once(SUBSDIR . '/Auth.subs.php'); |
1762
|
|
|
setLoginCookie(-3600, 0); |
1763
|
|
|
|
1764
|
|
|
if (isset($_SESSION['login_' . $cookiename])) |
1765
|
|
|
{ |
1766
|
|
|
unset($_SESSION['login_' . $cookiename]); |
1767
|
|
|
} |
1768
|
|
|
|
1769
|
|
|
// We need a member! |
1770
|
|
|
if ($id_member === 0) |
1771
|
|
|
{ |
1772
|
|
|
// Redirect back! |
1773
|
|
|
redirectexit(); |
1774
|
|
|
|
1775
|
|
|
// Probably not needed, but still make sure... |
1776
|
|
|
throw new \ElkArte\Exceptions\Exception('no_access', false); |
1777
|
|
|
} |
1778
|
|
|
|
1779
|
|
|
// Let's just initialize to something (and 0 is better than nothing) |
1780
|
|
|
$time_stamp = 0; |
1781
|
|
|
$number_tries = 0; |
1782
|
|
|
|
1783
|
|
|
// Right, have we got a flood value? |
1784
|
|
|
if ($password_flood_value !== false) |
1785
|
|
|
{ |
1786
|
|
|
@[$time_stamp, $number_tries] = explode('|', $password_flood_value); |
1787
|
|
|
} |
1788
|
|
|
|
1789
|
|
|
// Timestamp invalid or non-existent? |
1790
|
|
|
if (empty($number_tries) || $time_stamp < (time() - 10)) |
1791
|
|
|
{ |
1792
|
|
|
// If it wasn't *that* long ago, don't give them another five goes. |
1793
|
|
|
$number_tries = !empty($number_tries) && $time_stamp < (time() - 20) ? 2 : $number_tries; |
1794
|
|
|
$time_stamp = time(); |
1795
|
|
|
} |
1796
|
|
|
|
1797
|
|
|
$number_tries++; |
1798
|
|
|
|
1799
|
|
|
// Broken the law? |
1800
|
|
|
if ($number_tries > 5) |
1801
|
|
|
{ |
1802
|
|
|
throw new \ElkArte\Exceptions\Exception('login_threshold_brute_fail', 'critical'); |
1803
|
|
|
} |
1804
|
|
|
|
1805
|
|
|
// Otherwise set the members data. If they correct on their first attempt then we actually clear it, otherwise we set it! |
1806
|
|
|
require_once(SUBSDIR . '/Members.subs.php'); |
1807
|
|
|
updateMemberData($id_member, ['passwd_flood' => $was_correct && $number_tries == 1 ? '' : $time_stamp . '|' . $number_tries]); |
1808
|
|
|
} |
1809
|
|
|
|
1810
|
|
|
/** |
1811
|
|
|
* This sets the X-Frame-Options header. |
1812
|
|
|
* |
1813
|
|
|
* @param string|null $override the frame option, defaults to deny. |
1814
|
|
|
*/ |
1815
|
|
|
function frameOptionsHeader($override = null) |
1816
|
|
|
{ |
1817
|
|
|
global $modSettings; |
1818
|
|
|
|
1819
|
|
|
$option = 'SAMEORIGIN'; |
1820
|
|
|
|
1821
|
|
|
if (is_null($override) && !empty($modSettings['frame_security'])) |
1822
|
|
|
{ |
1823
|
|
|
$option = $modSettings['frame_security']; |
1824
|
|
|
} |
1825
|
|
|
elseif (in_array($override, ['SAMEORIGIN', 'DENY'])) |
1826
|
|
|
{ |
1827
|
|
|
$option = $override; |
1828
|
|
|
} |
1829
|
|
|
|
1830
|
|
|
// Don't bother setting the header if we have disabled it. |
1831
|
|
|
if ($option === 'DISABLE') |
1832
|
|
|
{ |
1833
|
|
|
return; |
1834
|
|
|
} |
1835
|
|
|
|
1836
|
|
|
// Finally set it. |
1837
|
|
|
Headers::instance()->header('X-Frame-Options', $option); |
1838
|
|
|
} |
1839
|
|
|
|
1840
|
|
|
/** |
1841
|
|
|
* This adds additional security headers that may prevent browsers from doing something they should not |
1842
|
|
|
* |
1843
|
|
|
* What it does: |
1844
|
|
|
* |
1845
|
|
|
* - X-XSS-Protection header - This header enables the Cross-site scripting (XSS) filter |
1846
|
|
|
* built into most recent web browsers. It's usually enabled by default, so the role of this |
1847
|
|
|
* header is to re-enable the filter for this particular website if it was disabled by the user. |
1848
|
|
|
* - X-Content-Type-Options header - It prevents the browser from doing MIME-type sniffing, |
1849
|
|
|
* only IE and Chrome are honouring this header. This reduces exposure to drive-by download attacks |
1850
|
|
|
* and sites serving user uploaded content that could be treated as executable or dynamic HTML files. |
1851
|
|
|
* |
1852
|
|
|
* @param bool|null $override |
1853
|
|
|
*/ |
1854
|
|
|
function securityOptionsHeader($override = null) |
1855
|
|
|
{ |
1856
|
|
|
if ($override !== true) |
1857
|
|
|
{ |
1858
|
|
|
Headers::instance() |
1859
|
|
|
->header('X-XSS-Protection', '1') |
1860
|
|
|
->header('X-Content-Type-Options', 'nosniff'); |
1861
|
|
|
} |
1862
|
|
|
} |
1863
|
|
|
|
1864
|
|
|
/** |
1865
|
|
|
* Stop some browsers pre fetching activity to reduce server load |
1866
|
|
|
*/ |
1867
|
|
|
function stop_prefetching() |
1868
|
|
|
{ |
1869
|
|
|
if ((isset($_SERVER['HTTP_PURPOSE']) && $_SERVER['HTTP_PURPOSE'] === 'prefetch') |
1870
|
|
|
|| (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] === 'prefetch')) |
1871
|
|
|
{ |
1872
|
|
|
@ob_end_clean(); |
1873
|
|
|
Headers::instance() |
1874
|
|
|
->removeHeader('all') |
1875
|
|
|
->headerSpecial('HTTP/1.1 403 Prefetch Forbidden') |
1876
|
|
|
->sendHeaders(); |
1877
|
|
|
die; |
1878
|
|
|
} |
1879
|
|
|
} |
1880
|
24 |
|
|
1881
|
24 |
|
/** |
1882
|
|
|
* Check if the admin's session is active |
1883
|
|
|
* |
1884
|
|
|
* @return bool |
1885
|
|
|
*/ |
1886
|
|
|
function isAdminSessionActive() |
1887
|
24 |
|
{ |
1888
|
|
|
global $modSettings; |
1889
|
|
|
|
1890
|
|
|
return empty($modSettings['securityDisable']) && (isset($_SESSION['admin_time']) && $_SESSION['admin_time'] + ($modSettings['admin_session_lifetime'] * 60) > time()); |
1891
|
|
|
} |
1892
|
|
|
|
1893
|
|
|
/** |
1894
|
|
|
* Check if security files exist |
1895
|
|
|
* |
1896
|
|
|
* If files are found, populate $context['security_controls_files']: |
1897
|
|
|
* * 'title' - $txt['security_risk'] |
1898
|
|
|
* * 'errors' - An array of strings with the key being the filename and the value an error with the filename in it |
1899
|
|
|
* |
1900
|
|
|
* @event integrate_security_files Allows adding / modifying security files array |
1901
|
|
|
* |
1902
|
|
|
* @return bool |
1903
|
|
|
*/ |
1904
|
|
|
function checkSecurityFiles() |
1905
|
|
|
{ |
1906
|
|
|
global $txt, $context; |
1907
|
|
|
|
1908
|
|
|
$has_files = false; |
1909
|
|
|
|
1910
|
|
|
$securityFiles = ['install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~']; |
1911
|
|
|
call_integration_hook('integrate_security_files', [&$securityFiles]); |
1912
|
|
|
|
1913
|
|
|
foreach ($securityFiles as $securityFile) |
1914
|
|
|
{ |
1915
|
|
|
if (file_exists(BOARDDIR . '/' . $securityFile)) |
1916
|
|
|
{ |
1917
|
|
|
$has_files = true; |
1918
|
|
|
|
1919
|
|
|
$context['security_controls_files']['title'] = $txt['security_risk']; |
1920
|
|
|
$context['security_controls_files']['errors'][$securityFile] = sprintf($txt['not_removed'], $securityFile); |
1921
|
|
|
|
1922
|
|
|
if ($securityFile === 'Settings.php~' || $securityFile === 'Settings_bak.php~') |
1923
|
|
|
{ |
1924
|
|
|
$context['security_controls_files']['errors'][$securityFile] .= '<span class="smalltext">' . sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)) . '</span>'; |
1925
|
|
|
} |
1926
|
|
|
} |
1927
|
|
|
} |
1928
|
|
|
|
1929
|
|
|
return $has_files; |
1930
|
|
|
} |
1931
|
|
|
|
1932
|
|
|
/** |
1933
|
|
|
* The login URL should not redirect to certain areas (attachments, js actions, etc) |
1934
|
|
|
* this function does these checks and return if the URL is valid or not. |
1935
|
|
|
* |
1936
|
|
|
* @param string $url - The URL to validate |
1937
|
|
|
* @param bool $match_board - If true tries to match board|topic in the URL as well |
1938
|
|
|
* @return bool |
1939
|
|
|
*/ |
1940
|
|
|
function validLoginUrl($url, $match_board = false) |
1941
|
|
|
{ |
1942
|
|
|
if (empty($url)) |
1943
|
|
|
{ |
1944
|
|
|
return false; |
1945
|
|
|
} |
1946
|
|
|
|
1947
|
|
|
if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) |
1948
|
|
|
{ |
1949
|
|
|
return false; |
1950
|
2 |
|
} |
1951
|
|
|
|
1952
|
|
|
$invalid_strings = ['dlattach' => '~(board|topic)[=,]~', 'jslocale' => '', 'login' => '']; |
1953
|
|
|
call_integration_hook('integrate_validLoginUrl', [&$invalid_strings]); |
1954
|
|
|
|
1955
|
2 |
|
foreach ($invalid_strings as $invalid_string => $valid_match) |
1956
|
|
|
{ |
1957
|
|
|
if (strpos($url, $invalid_string) !== false |
1958
|
|
|
|| ($match_board === true && !empty($valid_match) && preg_match($valid_match, $url) !== 1)) |
1959
|
|
|
{ |
1960
|
2 |
|
return false; |
1961
|
2 |
|
} |
1962
|
|
|
} |
1963
|
2 |
|
|
1964
|
|
|
return true; |
1965
|
|
|
} |
1966
|
|
|
|
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
orexit
statements that have been added for debug purposes.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.